mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 17:30:06 +00:00
Merge branch 'main' into ENG-2685
This commit is contained in:
@@ -34,6 +34,7 @@ import { registerSnapshotRouter } from "./snapshot-router";
|
||||
import { registerSshCaRouter } from "./ssh-certificate-authority-router";
|
||||
import { registerSshCertRouter } from "./ssh-certificate-router";
|
||||
import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router";
|
||||
import { registerSshHostGroupRouter } from "./ssh-host-group-router";
|
||||
import { registerSshHostRouter } from "./ssh-host-router";
|
||||
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
||||
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
||||
@@ -88,6 +89,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
await sshRouter.register(registerSshCertRouter, { prefix: "/certificates" });
|
||||
await sshRouter.register(registerSshCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||
await sshRouter.register(registerSshHostRouter, { prefix: "/hosts" });
|
||||
await sshRouter.register(registerSshHostGroupRouter, { prefix: "/host-groups" });
|
||||
},
|
||||
{ prefix: "/ssh" }
|
||||
);
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema";
|
||||
import { sanitizedSshHostGroup } from "@app/ee/services/ssh-host-group/ssh-host-group-schema";
|
||||
import { EHostGroupMembershipFilter } from "@app/ee/services/ssh-host-group/ssh-host-group-types";
|
||||
import { ApiDocsTags, SSH_HOST_GROUPS } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSshHostGroupRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:sshHostGroupId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHostGroups],
|
||||
description: "Get SSH Host Group",
|
||||
params: z.object({
|
||||
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.GET.sshHostGroupId)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedSshHostGroup.extend({
|
||||
loginMappings: z.array(loginMappingSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const sshHostGroup = await server.services.sshHostGroup.getSshHostGroup({
|
||||
sshHostGroupId: req.params.sshHostGroupId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: sshHostGroup.projectId,
|
||||
event: {
|
||||
type: EventType.GET_SSH_HOST_GROUP,
|
||||
metadata: {
|
||||
sshHostGroupId: sshHostGroup.id,
|
||||
name: sshHostGroup.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return sshHostGroup;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHostGroups],
|
||||
description: "Create SSH Host Group",
|
||||
body: z.object({
|
||||
projectId: z.string().describe(SSH_HOST_GROUPS.CREATE.projectId),
|
||||
name: slugSchema({ min: 1, max: 64, field: "name" }).describe(SSH_HOST_GROUPS.CREATE.name),
|
||||
loginMappings: z.array(loginMappingSchema).default([]).describe(SSH_HOST_GROUPS.CREATE.loginMappings)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedSshHostGroup.extend({
|
||||
loginMappings: z.array(loginMappingSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const sshHostGroup = await server.services.sshHostGroup.createSshHostGroup({
|
||||
...req.body,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: sshHostGroup.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_SSH_HOST_GROUP,
|
||||
metadata: {
|
||||
sshHostGroupId: sshHostGroup.id,
|
||||
name: sshHostGroup.name,
|
||||
loginMappings: sshHostGroup.loginMappings
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return sshHostGroup;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:sshHostGroupId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHostGroups],
|
||||
description: "Update SSH Host Group",
|
||||
params: z.object({
|
||||
sshHostGroupId: z.string().trim().describe(SSH_HOST_GROUPS.UPDATE.sshHostGroupId)
|
||||
}),
|
||||
body: z.object({
|
||||
name: slugSchema({ min: 1, max: 64, field: "name" }).describe(SSH_HOST_GROUPS.UPDATE.name).optional(),
|
||||
loginMappings: z.array(loginMappingSchema).optional().describe(SSH_HOST_GROUPS.UPDATE.loginMappings)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedSshHostGroup.extend({
|
||||
loginMappings: z.array(loginMappingSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const sshHostGroup = await server.services.sshHostGroup.updateSshHostGroup({
|
||||
sshHostGroupId: req.params.sshHostGroupId,
|
||||
...req.body,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: sshHostGroup.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_SSH_HOST_GROUP,
|
||||
metadata: {
|
||||
sshHostGroupId: sshHostGroup.id,
|
||||
name: sshHostGroup.name,
|
||||
loginMappings: sshHostGroup.loginMappings
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return sshHostGroup;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:sshHostGroupId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHostGroups],
|
||||
description: "Delete SSH Host Group",
|
||||
params: z.object({
|
||||
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.DELETE.sshHostGroupId)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedSshHostGroup.extend({
|
||||
loginMappings: z.array(loginMappingSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const sshHostGroup = await server.services.sshHostGroup.deleteSshHostGroup({
|
||||
sshHostGroupId: req.params.sshHostGroupId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: sshHostGroup.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_SSH_HOST_GROUP,
|
||||
metadata: {
|
||||
sshHostGroupId: sshHostGroup.id,
|
||||
name: sshHostGroup.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return sshHostGroup;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:sshHostGroupId/hosts",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHostGroups],
|
||||
description: "Get SSH Hosts in a Host Group",
|
||||
params: z.object({
|
||||
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.GET.sshHostGroupId)
|
||||
}),
|
||||
querystring: z.object({
|
||||
filter: z.nativeEnum(EHostGroupMembershipFilter).optional().describe(SSH_HOST_GROUPS.GET.filter)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
hosts: sanitizedSshHost
|
||||
.pick({
|
||||
id: true,
|
||||
hostname: true,
|
||||
alias: true
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
isPartOfGroup: z.boolean(),
|
||||
joinedGroupAt: z.date().nullable()
|
||||
})
|
||||
)
|
||||
.array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { sshHostGroup, hosts, totalCount } = await server.services.sshHostGroup.listSshHostGroupHosts({
|
||||
sshHostGroupId: req.params.sshHostGroupId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: sshHostGroup.projectId,
|
||||
event: {
|
||||
type: EventType.GET_SSH_HOST_GROUP_HOSTS,
|
||||
metadata: {
|
||||
sshHostGroupId: req.params.sshHostGroupId,
|
||||
name: sshHostGroup.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { hosts, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:sshHostGroupId/hosts/:hostId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHostGroups],
|
||||
description: "Add an SSH Host to a Host Group",
|
||||
params: z.object({
|
||||
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.ADD_HOST.sshHostGroupId),
|
||||
hostId: z.string().describe(SSH_HOST_GROUPS.ADD_HOST.hostId)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedSshHost.extend({
|
||||
loginMappings: z.array(loginMappingSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { sshHostGroup, sshHost } = await server.services.sshHostGroup.addHostToSshHostGroup({
|
||||
sshHostGroupId: req.params.sshHostGroupId,
|
||||
hostId: req.params.hostId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: sshHost.projectId,
|
||||
event: {
|
||||
type: EventType.ADD_HOST_TO_SSH_HOST_GROUP,
|
||||
metadata: {
|
||||
sshHostGroupId: sshHostGroup.id,
|
||||
sshHostId: sshHost.id,
|
||||
hostname: sshHost.hostname
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return sshHost;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:sshHostGroupId/hosts/:hostId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHostGroups],
|
||||
description: "Remove an SSH Host from a Host Group",
|
||||
params: z.object({
|
||||
sshHostGroupId: z.string().describe(SSH_HOST_GROUPS.DELETE_HOST.sshHostGroupId),
|
||||
hostId: z.string().describe(SSH_HOST_GROUPS.DELETE_HOST.hostId)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedSshHost.extend({
|
||||
loginMappings: z.array(loginMappingSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { sshHostGroup, sshHost } = await server.services.sshHostGroup.removeHostFromSshHostGroup({
|
||||
sshHostGroupId: req.params.sshHostGroupId,
|
||||
hostId: req.params.hostId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: sshHost.projectId,
|
||||
event: {
|
||||
type: EventType.REMOVE_HOST_FROM_SSH_HOST_GROUP,
|
||||
metadata: {
|
||||
sshHostGroupId: sshHostGroup.id,
|
||||
sshHostId: sshHost.id,
|
||||
hostname: sshHost.hostname
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return sshHost;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -3,8 +3,9 @@ import { z } from "zod";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||
import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema";
|
||||
import { LoginMappingSource } from "@app/ee/services/ssh-host/ssh-host-types";
|
||||
import { isValidHostname } from "@app/ee/services/ssh-host/ssh-host-validators";
|
||||
import { SSH_HOSTS } from "@app/lib/api-docs";
|
||||
import { ApiDocsTags, SSH_HOSTS } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { publicSshCaLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
@@ -21,10 +22,16 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHosts],
|
||||
response: {
|
||||
200: z.array(
|
||||
sanitizedSshHost.extend({
|
||||
loginMappings: z.array(loginMappingSchema)
|
||||
loginMappings: loginMappingSchema
|
||||
.extend({
|
||||
source: z.nativeEnum(LoginMappingSource)
|
||||
})
|
||||
.array()
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -49,12 +56,18 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHosts],
|
||||
params: z.object({
|
||||
sshHostId: z.string().describe(SSH_HOSTS.GET.sshHostId)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedSshHost.extend({
|
||||
loginMappings: z.array(loginMappingSchema)
|
||||
loginMappings: loginMappingSchema
|
||||
.extend({
|
||||
source: z.nativeEnum(LoginMappingSource)
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -91,7 +104,9 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Add an SSH Host",
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHosts],
|
||||
description: "Register SSH Host",
|
||||
body: z.object({
|
||||
projectId: z.string().describe(SSH_HOSTS.CREATE.projectId),
|
||||
hostname: z
|
||||
@@ -119,7 +134,11 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedSshHost.extend({
|
||||
loginMappings: z.array(loginMappingSchema)
|
||||
loginMappings: loginMappingSchema
|
||||
.extend({
|
||||
source: z.nativeEnum(LoginMappingSource)
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -163,6 +182,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHosts],
|
||||
description: "Update SSH Host",
|
||||
params: z.object({
|
||||
sshHostId: z.string().trim().describe(SSH_HOSTS.UPDATE.sshHostId)
|
||||
@@ -192,7 +213,11 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedSshHost.extend({
|
||||
loginMappings: z.array(loginMappingSchema)
|
||||
loginMappings: loginMappingSchema
|
||||
.extend({
|
||||
source: z.nativeEnum(LoginMappingSource)
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -235,12 +260,19 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHosts],
|
||||
description: "Delete SSH Host",
|
||||
params: z.object({
|
||||
sshHostId: z.string().describe(SSH_HOSTS.DELETE.sshHostId)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedSshHost.extend({
|
||||
loginMappings: z.array(loginMappingSchema)
|
||||
loginMappings: loginMappingSchema
|
||||
.extend({
|
||||
source: z.nativeEnum(LoginMappingSource)
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -278,6 +310,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHosts],
|
||||
description: "Issue SSH certificate for user",
|
||||
params: z.object({
|
||||
sshHostId: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.sshHostId)
|
||||
@@ -350,6 +384,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHosts],
|
||||
description: "Issue SSH certificate for host",
|
||||
params: z.object({
|
||||
sshHostId: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.sshHostId)
|
||||
@@ -414,6 +450,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: publicSshCaLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHosts],
|
||||
description: "Get public key of the user SSH CA linked to the host",
|
||||
params: z.object({
|
||||
sshHostId: z.string().trim().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.sshHostId)
|
||||
@@ -435,6 +473,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: publicSshCaLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHosts],
|
||||
description: "Get public key of the host SSH CA linked to the host",
|
||||
params: z.object({
|
||||
sshHostId: z.string().trim().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.sshHostId)
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
AzureClientSecretRotationGeneratedCredentialsSchema,
|
||||
AzureClientSecretRotationSchema,
|
||||
CreateAzureClientSecretRotationSchema,
|
||||
UpdateAzureClientSecretRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/azure-client-secret";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
|
||||
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
|
||||
|
||||
export const registerAzureClientSecretRotationRouter = async (server: FastifyZodProvider) =>
|
||||
registerSecretRotationEndpoints({
|
||||
type: SecretRotation.AzureClientSecret,
|
||||
server,
|
||||
responseSchema: AzureClientSecretRotationSchema,
|
||||
createSchema: CreateAzureClientSecretRotationSchema,
|
||||
updateSchema: UpdateAzureClientSecretRotationSchema,
|
||||
generatedCredentialsSchema: AzureClientSecretRotationGeneratedCredentialsSchema
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotat
|
||||
|
||||
import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router";
|
||||
import { registerAwsIamUserSecretRotationRouter } from "./aws-iam-user-secret-rotation-router";
|
||||
import { registerAzureClientSecretRotationRouter } from "./azure-client-secret-rotation-router";
|
||||
import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router";
|
||||
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
|
||||
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
|
||||
@@ -15,6 +16,7 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
|
||||
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
|
||||
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
|
||||
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
|
||||
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
|
||||
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter
|
||||
[SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter,
|
||||
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter,
|
||||
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { z } from "zod";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
||||
import { AwsIamUserSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
|
||||
import { AzureClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/azure-client-secret";
|
||||
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
@@ -16,8 +17,9 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
|
||||
PostgresCredentialsRotationListItemSchema,
|
||||
MsSqlCredentialsRotationListItemSchema,
|
||||
Auth0ClientSecretRotationListItemSchema,
|
||||
LdapPasswordRotationListItemSchema,
|
||||
AwsIamUserSecretRotationListItemSchema
|
||||
AzureClientSecretRotationListItemSchema,
|
||||
AwsIamUserSecretRotationListItemSchema,
|
||||
LdapPasswordRotationListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
|
||||
|
||||
+32
-20
@@ -6,13 +6,15 @@ import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
|
||||
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
||||
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
|
||||
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
@@ -67,6 +69,8 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
>;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
};
|
||||
|
||||
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
|
||||
@@ -84,6 +88,8 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
smtpService,
|
||||
userDAL,
|
||||
kmsService,
|
||||
microsoftTeamsService,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
projectSlackConfigDAL
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const createAccessApprovalRequest = async ({
|
||||
@@ -219,24 +225,30 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
||||
const approvalUrl = `${cfg.SITE_URL}/secret-manager/${project.id}/approval`;
|
||||
|
||||
await triggerSlackNotification({
|
||||
projectId: project.id,
|
||||
projectSlackConfigDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
notification: {
|
||||
type: SlackTriggerFeature.ACCESS_REQUEST,
|
||||
payload: {
|
||||
projectName: project.name,
|
||||
requesterFullName,
|
||||
isTemporary,
|
||||
requesterEmail: requestedByUser.email as string,
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl,
|
||||
note
|
||||
}
|
||||
await triggerWorkflowIntegrationNotification({
|
||||
input: {
|
||||
notification: {
|
||||
type: TriggerFeature.ACCESS_REQUEST,
|
||||
payload: {
|
||||
projectName: project.name,
|
||||
requesterFullName,
|
||||
isTemporary,
|
||||
requesterEmail: requestedByUser.email as string,
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl,
|
||||
note
|
||||
}
|
||||
},
|
||||
projectId: project.id
|
||||
},
|
||||
dependencies: {
|
||||
projectDAL,
|
||||
projectSlackConfigDAL,
|
||||
kmsService,
|
||||
microsoftTeamsService,
|
||||
projectMicrosoftTeamsConfigDAL
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||
import { TLoginMapping } from "@app/ee/services/ssh-host/ssh-host-types";
|
||||
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign/types";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
TSecretSyncRaw,
|
||||
TUpdateSecretSyncDTO
|
||||
} from "@app/services/secret-sync/secret-sync-types";
|
||||
import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types";
|
||||
|
||||
import { KmipPermission } from "../kmip/kmip-enum";
|
||||
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
|
||||
@@ -191,12 +193,19 @@ export enum EventType {
|
||||
UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template",
|
||||
DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template",
|
||||
GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template",
|
||||
GET_SSH_HOST = "get-ssh-host",
|
||||
CREATE_SSH_HOST = "create-ssh-host",
|
||||
UPDATE_SSH_HOST = "update-ssh-host",
|
||||
DELETE_SSH_HOST = "delete-ssh-host",
|
||||
GET_SSH_HOST = "get-ssh-host",
|
||||
ISSUE_SSH_HOST_USER_CERT = "issue-ssh-host-user-cert",
|
||||
ISSUE_SSH_HOST_HOST_CERT = "issue-ssh-host-host-cert",
|
||||
GET_SSH_HOST_GROUP = "get-ssh-host-group",
|
||||
CREATE_SSH_HOST_GROUP = "create-ssh-host-group",
|
||||
UPDATE_SSH_HOST_GROUP = "update-ssh-host-group",
|
||||
DELETE_SSH_HOST_GROUP = "delete-ssh-host-group",
|
||||
GET_SSH_HOST_GROUP_HOSTS = "get-ssh-host-group-hosts",
|
||||
ADD_HOST_TO_SSH_HOST_GROUP = "add-host-to-ssh-host-group",
|
||||
REMOVE_HOST_FROM_SSH_HOST_GROUP = "remove-host-from-ssh-host-group",
|
||||
CREATE_CA = "create-certificate-authority",
|
||||
GET_CA = "get-certificate-authority",
|
||||
UPDATE_CA = "update-certificate-authority",
|
||||
@@ -246,11 +255,14 @@ export enum EventType {
|
||||
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
|
||||
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
|
||||
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
|
||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
||||
GET_SLACK_INTEGRATION = "get-slack-integration",
|
||||
UPDATE_SLACK_INTEGRATION = "update-slack-integration",
|
||||
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
|
||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
||||
GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "get-project-workflow-integration-config",
|
||||
UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "update-project-workflow-integration-config",
|
||||
|
||||
GET_PROJECT_SSH_CONFIG = "get-project-ssh-config",
|
||||
UPDATE_PROJECT_SSH_CONFIG = "update-project-ssh-config",
|
||||
INTEGRATION_SYNCED = "integration-synced",
|
||||
@@ -323,6 +335,15 @@ export enum EventType {
|
||||
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets",
|
||||
|
||||
PROJECT_ACCESS_REQUEST = "project-access-request",
|
||||
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE = "microsoft-teams-workflow-integration-create",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE = "microsoft-teams-workflow-integration-delete",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE = "microsoft-teams-workflow-integration-update",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS = "microsoft-teams-workflow-integration-check-installation-status",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS = "microsoft-teams-workflow-integration-get-teams",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list",
|
||||
|
||||
PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start",
|
||||
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end"
|
||||
}
|
||||
@@ -1501,12 +1522,7 @@ interface CreateSshHost {
|
||||
alias: string | null;
|
||||
userCertTtl: string;
|
||||
hostCertTtl: string;
|
||||
loginMappings: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
loginMappings: TLoginMapping[];
|
||||
userSshCaId: string;
|
||||
hostSshCaId: string;
|
||||
};
|
||||
@@ -1520,12 +1536,7 @@ interface UpdateSshHost {
|
||||
alias?: string | null;
|
||||
userCertTtl?: string;
|
||||
hostCertTtl?: string;
|
||||
loginMappings?: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
loginMappings?: TLoginMapping[];
|
||||
userSshCaId?: string;
|
||||
hostSshCaId?: string;
|
||||
};
|
||||
@@ -1569,6 +1580,66 @@ interface IssueSshHostHostCert {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSshHostGroupEvent {
|
||||
type: EventType.GET_SSH_HOST_GROUP;
|
||||
metadata: {
|
||||
sshHostGroupId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSshHostGroupEvent {
|
||||
type: EventType.CREATE_SSH_HOST_GROUP;
|
||||
metadata: {
|
||||
sshHostGroupId: string;
|
||||
name: string;
|
||||
loginMappings: TLoginMapping[];
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateSshHostGroupEvent {
|
||||
type: EventType.UPDATE_SSH_HOST_GROUP;
|
||||
metadata: {
|
||||
sshHostGroupId: string;
|
||||
name?: string;
|
||||
loginMappings?: TLoginMapping[];
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteSshHostGroupEvent {
|
||||
type: EventType.DELETE_SSH_HOST_GROUP;
|
||||
metadata: {
|
||||
sshHostGroupId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSshHostGroupHostsEvent {
|
||||
type: EventType.GET_SSH_HOST_GROUP_HOSTS;
|
||||
metadata: {
|
||||
sshHostGroupId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddHostToSshHostGroupEvent {
|
||||
type: EventType.ADD_HOST_TO_SSH_HOST_GROUP;
|
||||
metadata: {
|
||||
sshHostGroupId: string;
|
||||
sshHostId: string;
|
||||
hostname: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface RemoveHostFromSshHostGroupEvent {
|
||||
type: EventType.REMOVE_HOST_FROM_SSH_HOST_GROUP;
|
||||
metadata: {
|
||||
sshHostGroupId: string;
|
||||
sshHostId: string;
|
||||
hostname: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateCa {
|
||||
type: EventType.CREATE_CA;
|
||||
metadata: {
|
||||
@@ -2000,22 +2071,24 @@ interface GetSlackIntegration {
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateProjectSlackConfig {
|
||||
type: EventType.UPDATE_PROJECT_SLACK_CONFIG;
|
||||
interface UpdateProjectWorkflowIntegrationConfig {
|
||||
type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
|
||||
metadata: {
|
||||
id: string;
|
||||
slackIntegrationId: string;
|
||||
integrationId: string;
|
||||
integration: WorkflowIntegration;
|
||||
isAccessRequestNotificationEnabled: boolean;
|
||||
accessRequestChannels: string;
|
||||
accessRequestChannels?: string | { teamId: string; channelIds: string[] };
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
secretRequestChannels: string;
|
||||
secretRequestChannels?: string | { teamId: string; channelIds: string[] };
|
||||
};
|
||||
}
|
||||
|
||||
interface GetProjectSlackConfig {
|
||||
type: EventType.GET_PROJECT_SLACK_CONFIG;
|
||||
interface GetProjectWorkflowIntegrationConfig {
|
||||
type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
|
||||
metadata: {
|
||||
id: string;
|
||||
integration: WorkflowIntegration;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2581,6 +2654,66 @@ interface RotateSecretRotationEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface MicrosoftTeamsWorkflowIntegrationCreateEvent {
|
||||
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE;
|
||||
metadata: {
|
||||
tenantId: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MicrosoftTeamsWorkflowIntegrationDeleteEvent {
|
||||
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE;
|
||||
metadata: {
|
||||
tenantId: string;
|
||||
id: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MicrosoftTeamsWorkflowIntegrationCheckInstallationStatusEvent {
|
||||
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS;
|
||||
metadata: {
|
||||
tenantId: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MicrosoftTeamsWorkflowIntegrationGetTeamsEvent {
|
||||
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS;
|
||||
metadata: {
|
||||
tenantId: string;
|
||||
slug: string;
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MicrosoftTeamsWorkflowIntegrationGetEvent {
|
||||
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET;
|
||||
metadata: {
|
||||
tenantId: string;
|
||||
slug: string;
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MicrosoftTeamsWorkflowIntegrationListEvent {
|
||||
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST;
|
||||
metadata: Record<string, string>;
|
||||
}
|
||||
|
||||
interface MicrosoftTeamsWorkflowIntegrationUpdateEvent {
|
||||
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE;
|
||||
metadata: {
|
||||
tenantId: string;
|
||||
slug: string;
|
||||
id: string;
|
||||
newSlug?: string;
|
||||
newDescription?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -2745,8 +2878,8 @@ export type Event =
|
||||
| UpdateSlackIntegration
|
||||
| DeleteSlackIntegration
|
||||
| GetSlackIntegration
|
||||
| UpdateProjectSlackConfig
|
||||
| GetProjectSlackConfig
|
||||
| UpdateProjectWorkflowIntegrationConfig
|
||||
| GetProjectWorkflowIntegrationConfig
|
||||
| GetProjectSshConfig
|
||||
| UpdateProjectSshConfig
|
||||
| IntegrationSyncedEvent
|
||||
@@ -2775,6 +2908,13 @@ export type Event =
|
||||
| CreateAppConnectionEvent
|
||||
| UpdateAppConnectionEvent
|
||||
| DeleteAppConnectionEvent
|
||||
| GetSshHostGroupEvent
|
||||
| CreateSshHostGroupEvent
|
||||
| UpdateSshHostGroupEvent
|
||||
| DeleteSshHostGroupEvent
|
||||
| GetSshHostGroupHostsEvent
|
||||
| AddHostToSshHostGroupEvent
|
||||
| RemoveHostFromSshHostGroupEvent
|
||||
| CreateSharedSecretEvent
|
||||
| DeleteSharedSecretEvent
|
||||
| ReadSharedSecretEvent
|
||||
@@ -2816,4 +2956,11 @@ export type Event =
|
||||
| CreateSecretRotationEvent
|
||||
| UpdateSecretRotationEvent
|
||||
| DeleteSecretRotationEvent
|
||||
| RotateSecretRotationEvent;
|
||||
| RotateSecretRotationEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationCreateEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationDeleteEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationCheckInstallationStatusEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationGetTeamsEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationGetEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationListEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationUpdateEvent;
|
||||
|
||||
@@ -153,7 +153,7 @@ export const groupDALFactory = (db: TDbClient) => {
|
||||
totalCount: Number(members?.[0]?.total_count ?? 0)
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find all org members" });
|
||||
throw new DatabaseError({ error, name: "Find all user group members" });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ export const getDefaultOnPremFeatures = () => {
|
||||
has_used_trial: true,
|
||||
secretApproval: true,
|
||||
secretRotation: true,
|
||||
caCrl: false
|
||||
caCrl: false,
|
||||
sshHostGroups: false
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export const BillingPlanRows = {
|
||||
CustomAlerts: { name: "Custom alerts", field: "customAlerts" },
|
||||
AuditLogs: { name: "Audit logs", field: "auditLogs" },
|
||||
SamlSSO: { name: "SAML SSO", field: "samlSSO" },
|
||||
SshHostGroups: { name: "SSH Host Groups", field: "sshHostGroups" },
|
||||
Hsm: { name: "Hardware Security Module (HSM)", field: "hsm" },
|
||||
OidcSSO: { name: "OIDC SSO", field: "oidcSSO" },
|
||||
SecretApproval: { name: "Secret approvals", field: "secretApproval" },
|
||||
|
||||
@@ -53,7 +53,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
enforceMfa: false,
|
||||
projectTemplates: false,
|
||||
kmip: false,
|
||||
gateway: false
|
||||
gateway: false,
|
||||
sshHostGroups: false
|
||||
});
|
||||
|
||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||
|
||||
@@ -71,6 +71,7 @@ export type TFeatureSet = {
|
||||
projectTemplates: false;
|
||||
kmip: false;
|
||||
gateway: false;
|
||||
sshHostGroups: false;
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
||||
@@ -142,6 +142,7 @@ export enum ProjectPermissionSub {
|
||||
SshCertificates = "ssh-certificates",
|
||||
SshCertificateTemplates = "ssh-certificate-templates",
|
||||
SshHosts = "ssh-hosts",
|
||||
SshHostGroups = "ssh-host-groups",
|
||||
PkiAlerts = "pki-alerts",
|
||||
PkiCollections = "pki-collections",
|
||||
Kms = "kms",
|
||||
@@ -248,6 +249,7 @@ export type ProjectPermissionSet =
|
||||
ProjectPermissionSshHostActions,
|
||||
ProjectPermissionSub.SshHosts | (ForcedSubject<ProjectPermissionSub.SshHosts> & SshHostSubjectFields)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
||||
@@ -516,6 +518,12 @@ const GeneralPermissionSchema = [
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SshHostGroups).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
@@ -693,7 +701,8 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.PkiCollections,
|
||||
ProjectPermissionSub.SshCertificateAuthorities,
|
||||
ProjectPermissionSub.SshCertificates,
|
||||
ProjectPermissionSub.SshCertificateTemplates
|
||||
ProjectPermissionSub.SshCertificateTemplates,
|
||||
ProjectPermissionSub.SshHostGroups
|
||||
].forEach((el) => {
|
||||
can(
|
||||
[
|
||||
|
||||
+51
-31
@@ -17,9 +17,13 @@ import { groupBy, pick, unique } from "@app/lib/fn";
|
||||
import { setKnexStringValue } from "@app/lib/knex";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
|
||||
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
@@ -52,8 +56,6 @@ import {
|
||||
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
||||
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
||||
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
|
||||
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
@@ -126,6 +128,8 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||
};
|
||||
|
||||
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
||||
@@ -155,7 +159,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretVersionTagV2BridgeDAL,
|
||||
licenseService,
|
||||
projectSlackConfigDAL,
|
||||
resourceMetadataDAL
|
||||
resourceMetadataDAL,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
microsoftTeamsService
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
@@ -1171,21 +1177,28 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
|
||||
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
|
||||
await triggerSlackNotification({
|
||||
projectId,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
projectSlackConfigDAL,
|
||||
notification: {
|
||||
type: SlackTriggerFeature.SECRET_APPROVAL,
|
||||
payload: {
|
||||
userEmail: user.email as string,
|
||||
environment: env.name,
|
||||
secretPath,
|
||||
projectId,
|
||||
requestId: secretApprovalRequest.id,
|
||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
|
||||
|
||||
await triggerWorkflowIntegrationNotification({
|
||||
input: {
|
||||
projectId,
|
||||
notification: {
|
||||
type: TriggerFeature.SECRET_APPROVAL,
|
||||
payload: {
|
||||
userEmail: user.email as string,
|
||||
environment: env.name,
|
||||
secretPath,
|
||||
projectId,
|
||||
requestId: secretApprovalRequest.id,
|
||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
|
||||
}
|
||||
}
|
||||
},
|
||||
dependencies: {
|
||||
projectDAL,
|
||||
projectSlackConfigDAL,
|
||||
kmsService,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
microsoftTeamsService
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1503,21 +1516,28 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
|
||||
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
|
||||
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||
await triggerSlackNotification({
|
||||
projectId,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
projectSlackConfigDAL,
|
||||
notification: {
|
||||
type: SlackTriggerFeature.SECRET_APPROVAL,
|
||||
payload: {
|
||||
userEmail: user.email as string,
|
||||
environment: env.name,
|
||||
secretPath,
|
||||
projectId,
|
||||
requestId: secretApprovalRequest.id,
|
||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
|
||||
|
||||
await triggerWorkflowIntegrationNotification({
|
||||
input: {
|
||||
projectId,
|
||||
notification: {
|
||||
type: TriggerFeature.SECRET_APPROVAL,
|
||||
payload: {
|
||||
userEmail: user.email as string,
|
||||
environment: env.name,
|
||||
secretPath,
|
||||
projectId,
|
||||
requestId: secretApprovalRequest.id,
|
||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
|
||||
}
|
||||
}
|
||||
},
|
||||
dependencies: {
|
||||
projectDAL,
|
||||
kmsService,
|
||||
projectSlackConfigDAL,
|
||||
microsoftTeamsService,
|
||||
projectMicrosoftTeamsConfigDAL
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||
name: "Azure Client Secret",
|
||||
type: SecretRotation.AzureClientSecret,
|
||||
connection: AppConnection.AzureClientSecrets,
|
||||
template: {
|
||||
secretsMapping: {
|
||||
clientId: "AZURE_CLIENT_ID",
|
||||
clientSecret: "AZURE_CLIENT_SECRET"
|
||||
}
|
||||
}
|
||||
};
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import {
|
||||
AzureAddPasswordResponse,
|
||||
TAzureClientSecretRotationGeneratedCredentials,
|
||||
TAzureClientSecretRotationWithConnection
|
||||
} from "@app/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-types";
|
||||
import {
|
||||
TRotationFactory,
|
||||
TRotationFactoryGetSecretsPayload,
|
||||
TRotationFactoryIssueCredentials,
|
||||
TRotationFactoryRevokeCredentials,
|
||||
TRotationFactoryRotateCredentials
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-client-secrets";
|
||||
|
||||
const GRAPH_API_BASE = "https://graph.microsoft.com/v1.0";
|
||||
|
||||
type AzureErrorResponse = { error: { message: string } };
|
||||
|
||||
const sleep = async () =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
|
||||
export const azureClientSecretRotationFactory: TRotationFactory<
|
||||
TAzureClientSecretRotationWithConnection,
|
||||
TAzureClientSecretRotationGeneratedCredentials
|
||||
> = (secretRotation, appConnectionDAL, kmsService) => {
|
||||
const {
|
||||
connection,
|
||||
parameters: { objectId, clientId: clientIdParam },
|
||||
secretsMapping
|
||||
} = secretRotation;
|
||||
|
||||
/**
|
||||
* Creates a new client secret for the Azure app.
|
||||
*/
|
||||
const $rotateClientSecret = async () => {
|
||||
const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService);
|
||||
const endpoint = `${GRAPH_API_BASE}/applications/${objectId}/addPassword`;
|
||||
|
||||
const now = new Date();
|
||||
const formattedDate = `${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(
|
||||
2,
|
||||
"0"
|
||||
)}-${now.getFullYear()}`;
|
||||
|
||||
const endDateTime = new Date();
|
||||
endDateTime.setFullYear(now.getFullYear() + 5);
|
||||
|
||||
try {
|
||||
const { data } = await request.post<AzureAddPasswordResponse>(
|
||||
endpoint,
|
||||
{
|
||||
passwordCredential: {
|
||||
displayName: `Infisical Rotated Secret (${formattedDate})`,
|
||||
endDateTime: endDateTime.toISOString()
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!data?.secretText || !data?.keyId) {
|
||||
throw new Error("Invalid response from Azure: missing secretText or keyId.");
|
||||
}
|
||||
|
||||
return {
|
||||
clientSecret: data.secretText,
|
||||
keyId: data.keyId,
|
||||
clientId: clientIdParam
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
let message;
|
||||
if (
|
||||
error.response?.data &&
|
||||
typeof error.response.data === "object" &&
|
||||
"error" in error.response.data &&
|
||||
typeof (error.response.data as AzureErrorResponse).error.message === "string"
|
||||
) {
|
||||
message = (error.response.data as AzureErrorResponse).error.message;
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: `Failed to add client secret to Azure app ${objectId}: ${
|
||||
message || error.message || "Unknown error"
|
||||
}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Revokes a client secret from the Azure app using its keyId.
|
||||
*/
|
||||
const revokeCredential = async (keyId: string) => {
|
||||
const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService);
|
||||
const endpoint = `${GRAPH_API_BASE}/applications/${objectId}/removePassword`;
|
||||
|
||||
try {
|
||||
await request.post(
|
||||
endpoint,
|
||||
{ keyId },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
let message;
|
||||
if (
|
||||
error.response?.data &&
|
||||
typeof error.response.data === "object" &&
|
||||
"error" in error.response.data &&
|
||||
typeof (error.response.data as AzureErrorResponse).error.message === "string"
|
||||
) {
|
||||
message = (error.response.data as AzureErrorResponse).error.message;
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: `Failed to remove client secret with keyId ${keyId} from app ${objectId}: ${
|
||||
message || error.message || "Unknown error"
|
||||
}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Issues a new set of credentials.
|
||||
*/
|
||||
const issueCredentials: TRotationFactoryIssueCredentials<TAzureClientSecretRotationGeneratedCredentials> = async (
|
||||
callback
|
||||
) => {
|
||||
const credentials = await $rotateClientSecret();
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
/**
|
||||
* Revokes a list of credentials.
|
||||
*/
|
||||
const revokeCredentials: TRotationFactoryRevokeCredentials<TAzureClientSecretRotationGeneratedCredentials> = async (
|
||||
credentials,
|
||||
callback
|
||||
) => {
|
||||
if (!credentials?.length) return callback();
|
||||
|
||||
for (const { keyId } of credentials) {
|
||||
await revokeCredential(keyId);
|
||||
await sleep();
|
||||
}
|
||||
return callback();
|
||||
};
|
||||
|
||||
/**
|
||||
* Rotates credentials by issuing new ones and revoking the old.
|
||||
*/
|
||||
const rotateCredentials: TRotationFactoryRotateCredentials<TAzureClientSecretRotationGeneratedCredentials> = async (
|
||||
oldCredentials,
|
||||
callback
|
||||
) => {
|
||||
const newCredentials = await $rotateClientSecret();
|
||||
if (oldCredentials?.keyId) {
|
||||
await revokeCredential(oldCredentials.keyId);
|
||||
}
|
||||
|
||||
return callback(newCredentials);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps the generated credentials into the secret payload format.
|
||||
*/
|
||||
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TAzureClientSecretRotationGeneratedCredentials> = ({
|
||||
clientSecret
|
||||
}) => [
|
||||
{ key: secretsMapping.clientSecret, value: clientSecret },
|
||||
{ key: secretsMapping.clientId, value: clientIdParam }
|
||||
];
|
||||
|
||||
return {
|
||||
issueCredentials,
|
||||
revokeCredentials,
|
||||
rotateCredentials,
|
||||
getSecretsPayload
|
||||
};
|
||||
};
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
BaseCreateSecretRotationSchema,
|
||||
BaseSecretRotationSchema,
|
||||
BaseUpdateSecretRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
import { SecretNameSchema } from "@app/server/lib/schemas";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const AzureClientSecretRotationGeneratedCredentialsSchema = z
|
||||
.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
keyId: z.string()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.max(2);
|
||||
|
||||
const AzureClientSecretRotationParametersSchema = z.object({
|
||||
objectId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Object ID Required")
|
||||
.describe(SecretRotations.PARAMETERS.AZURE_CLIENT_SECRET.objectId),
|
||||
appName: z.string().trim().describe(SecretRotations.PARAMETERS.AZURE_CLIENT_SECRET.appName).optional(),
|
||||
clientId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Client ID Required")
|
||||
.describe(SecretRotations.PARAMETERS.AZURE_CLIENT_SECRET.clientId)
|
||||
});
|
||||
|
||||
const AzureClientSecretRotationSecretsMappingSchema = z.object({
|
||||
clientId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AZURE_CLIENT_SECRET.clientId),
|
||||
clientSecret: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AZURE_CLIENT_SECRET.clientSecret)
|
||||
});
|
||||
|
||||
export const AzureClientSecretRotationTemplateSchema = z.object({
|
||||
secretsMapping: z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string()
|
||||
})
|
||||
});
|
||||
|
||||
export const AzureClientSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.AzureClientSecret).extend({
|
||||
type: z.literal(SecretRotation.AzureClientSecret),
|
||||
parameters: AzureClientSecretRotationParametersSchema,
|
||||
secretsMapping: AzureClientSecretRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const CreateAzureClientSecretRotationSchema = BaseCreateSecretRotationSchema(
|
||||
SecretRotation.AzureClientSecret
|
||||
).extend({
|
||||
parameters: AzureClientSecretRotationParametersSchema,
|
||||
secretsMapping: AzureClientSecretRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const UpdateAzureClientSecretRotationSchema = BaseUpdateSecretRotationSchema(
|
||||
SecretRotation.AzureClientSecret
|
||||
).extend({
|
||||
parameters: AzureClientSecretRotationParametersSchema.optional(),
|
||||
secretsMapping: AzureClientSecretRotationSecretsMappingSchema.optional()
|
||||
});
|
||||
|
||||
export const AzureClientSecretRotationListItemSchema = z.object({
|
||||
name: z.literal("Azure Client Secret"),
|
||||
connection: z.literal(AppConnection.AzureClientSecrets),
|
||||
type: z.literal(SecretRotation.AzureClientSecret),
|
||||
template: AzureClientSecretRotationTemplateSchema
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TAzureClientSecretsConnection } from "@app/services/app-connection/azure-client-secrets";
|
||||
|
||||
import {
|
||||
AzureClientSecretRotationGeneratedCredentialsSchema,
|
||||
AzureClientSecretRotationListItemSchema,
|
||||
AzureClientSecretRotationSchema,
|
||||
CreateAzureClientSecretRotationSchema
|
||||
} from "./azure-client-secret-rotation-schemas";
|
||||
|
||||
export type TAzureClientSecretRotation = z.infer<typeof AzureClientSecretRotationSchema>;
|
||||
|
||||
export type TAzureClientSecretRotationInput = z.infer<typeof CreateAzureClientSecretRotationSchema>;
|
||||
|
||||
export type TAzureClientSecretRotationListItem = z.infer<typeof AzureClientSecretRotationListItemSchema>;
|
||||
|
||||
export type TAzureClientSecretRotationWithConnection = TAzureClientSecretRotation & {
|
||||
connection: TAzureClientSecretsConnection;
|
||||
};
|
||||
|
||||
export type TAzureClientSecretRotationGeneratedCredentials = z.infer<
|
||||
typeof AzureClientSecretRotationGeneratedCredentialsSchema
|
||||
>;
|
||||
|
||||
export interface TAzureClientSecretRotationParameters {
|
||||
appId: string;
|
||||
keyId?: string;
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
export interface TAzureClientSecretRotationSecretsMapping {
|
||||
appId: string;
|
||||
clientSecret: string;
|
||||
keyId: string;
|
||||
}
|
||||
|
||||
export interface AzureAddPasswordResponse {
|
||||
secretText: string;
|
||||
keyId: string;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./azure-client-secret-rotation-constants";
|
||||
export * from "./azure-client-secret-rotation-schemas";
|
||||
export * from "./azure-client-secret-rotation-types";
|
||||
@@ -2,8 +2,9 @@ export enum SecretRotation {
|
||||
PostgresCredentials = "postgres-credentials",
|
||||
MsSqlCredentials = "mssql-credentials",
|
||||
Auth0ClientSecret = "auth0-client-secret",
|
||||
LdapPassword = "ldap-password",
|
||||
AwsIamUserSecret = "aws-iam-user-secret"
|
||||
AzureClientSecret = "azure-client-secret",
|
||||
AwsIamUserSecret = "aws-iam-user-secret",
|
||||
LdapPassword = "ldap-password"
|
||||
}
|
||||
|
||||
export enum SecretRotationStatus {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret";
|
||||
import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret";
|
||||
import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-secret";
|
||||
import { LDAP_PASSWORD_ROTATION_LIST_OPTION } from "./ldap-password";
|
||||
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
|
||||
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
|
||||
@@ -21,8 +22,9 @@ const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2List
|
||||
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION
|
||||
[SecretRotation.AzureClientSecret]: AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretRotationOptions = () => {
|
||||
@@ -217,7 +219,7 @@ export const parseRotationErrorMessage = (err: unknown): string => {
|
||||
if (err instanceof AxiosError) {
|
||||
errorMessage += err?.response?.data
|
||||
? JSON.stringify(err?.response?.data)
|
||||
: err?.message ?? "An unknown error occurred.";
|
||||
: (err?.message ?? "An unknown error occurred.");
|
||||
} else {
|
||||
errorMessage += (err as Error)?.message || "An unknown error occurred.";
|
||||
}
|
||||
|
||||
@@ -5,14 +5,16 @@ export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
||||
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
|
||||
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Server Credentials",
|
||||
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret",
|
||||
[SecretRotation.LdapPassword]: "LDAP Password",
|
||||
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret"
|
||||
[SecretRotation.AzureClientSecret]: "Azure Client Secret",
|
||||
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret",
|
||||
[SecretRotation.LdapPassword]: "LDAP Password"
|
||||
};
|
||||
|
||||
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
||||
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
||||
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
|
||||
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
|
||||
[SecretRotation.LdapPassword]: AppConnection.LDAP,
|
||||
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS
|
||||
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
|
||||
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
|
||||
[SecretRotation.LdapPassword]: AppConnection.LDAP
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ export const BaseSecretRotationSchema = (type: SecretRotation) =>
|
||||
// unique to provider
|
||||
type: true,
|
||||
parameters: true,
|
||||
secretMappings: true
|
||||
secretsMapping: true
|
||||
}).extend({
|
||||
connection: z.object({
|
||||
app: z.literal(SECRET_ROTATION_CONNECTION_MAP[type]),
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns";
|
||||
import { azureClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-fns";
|
||||
import { ldapPasswordRotationFactory } from "@app/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns";
|
||||
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
@@ -102,7 +103,7 @@ export type TSecretRotationV2ServiceFactoryDep = {
|
||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
|
||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||
queueService: Pick<TQueueServiceFactory, "queuePg">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||
};
|
||||
|
||||
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
|
||||
@@ -117,8 +118,9 @@ const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplem
|
||||
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation
|
||||
[SecretRotation.AzureClientSecret]: azureClientSecretRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation
|
||||
};
|
||||
|
||||
export const secretRotationV2ServiceFactory = ({
|
||||
@@ -447,7 +449,8 @@ export const secretRotationV2ServiceFactory = ({
|
||||
{
|
||||
parameters: payload.parameters,
|
||||
secretsMapping,
|
||||
connection
|
||||
connection,
|
||||
rotationInterval: payload.rotationInterval
|
||||
} as TSecretRotationV2WithConnection,
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
|
||||
@@ -19,6 +19,13 @@ import {
|
||||
TAwsIamUserSecretRotationListItem,
|
||||
TAwsIamUserSecretRotationWithConnection
|
||||
} from "./aws-iam-user-secret";
|
||||
import {
|
||||
TAzureClientSecretRotation,
|
||||
TAzureClientSecretRotationGeneratedCredentials,
|
||||
TAzureClientSecretRotationInput,
|
||||
TAzureClientSecretRotationListItem,
|
||||
TAzureClientSecretRotationWithConnection
|
||||
} from "./azure-client-secret";
|
||||
import {
|
||||
TLdapPasswordRotation,
|
||||
TLdapPasswordRotationGeneratedCredentials,
|
||||
@@ -45,6 +52,7 @@ export type TSecretRotationV2 =
|
||||
| TPostgresCredentialsRotation
|
||||
| TMsSqlCredentialsRotation
|
||||
| TAuth0ClientSecretRotation
|
||||
| TAzureClientSecretRotation
|
||||
| TLdapPasswordRotation
|
||||
| TAwsIamUserSecretRotation;
|
||||
|
||||
@@ -52,12 +60,14 @@ export type TSecretRotationV2WithConnection =
|
||||
| TPostgresCredentialsRotationWithConnection
|
||||
| TMsSqlCredentialsRotationWithConnection
|
||||
| TAuth0ClientSecretRotationWithConnection
|
||||
| TAzureClientSecretRotationWithConnection
|
||||
| TLdapPasswordRotationWithConnection
|
||||
| TAwsIamUserSecretRotationWithConnection;
|
||||
|
||||
export type TSecretRotationV2GeneratedCredentials =
|
||||
| TSqlCredentialsRotationGeneratedCredentials
|
||||
| TAuth0ClientSecretRotationGeneratedCredentials
|
||||
| TAzureClientSecretRotationGeneratedCredentials
|
||||
| TLdapPasswordRotationGeneratedCredentials
|
||||
| TAwsIamUserSecretRotationGeneratedCredentials;
|
||||
|
||||
@@ -65,6 +75,7 @@ export type TSecretRotationV2Input =
|
||||
| TPostgresCredentialsRotationInput
|
||||
| TMsSqlCredentialsRotationInput
|
||||
| TAuth0ClientSecretRotationInput
|
||||
| TAzureClientSecretRotationInput
|
||||
| TLdapPasswordRotationInput
|
||||
| TAwsIamUserSecretRotationInput;
|
||||
|
||||
@@ -72,6 +83,7 @@ export type TSecretRotationV2ListItem =
|
||||
| TPostgresCredentialsRotationListItem
|
||||
| TMsSqlCredentialsRotationListItem
|
||||
| TAuth0ClientSecretRotationListItem
|
||||
| TAzureClientSecretRotationListItem
|
||||
| TLdapPasswordRotationListItem
|
||||
| TAwsIamUserSecretRotationListItem;
|
||||
|
||||
@@ -197,7 +209,7 @@ export type TRotationFactory<
|
||||
C extends TSecretRotationV2GeneratedCredentials
|
||||
> = (
|
||||
secretRotation: T,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
issueCredentials: TRotationFactoryIssueCredentials<C>;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
||||
import { AzureClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/azure-client-secret";
|
||||
import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
@@ -11,6 +12,7 @@ export const SecretRotationV2Schema = z.discriminatedUnion("type", [
|
||||
PostgresCredentialsRotationSchema,
|
||||
MsSqlCredentialsRotationSchema,
|
||||
Auth0ClientSecretRotationSchema,
|
||||
AzureClientSecretRotationSchema,
|
||||
LdapPasswordRotationSchema,
|
||||
AwsIamUserSecretRotationSchema
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||
import { groupBy, unique } from "@app/lib/fn";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
import { EHostGroupMembershipFilter } from "./ssh-host-group-types";
|
||||
|
||||
export type TSshHostGroupDALFactory = ReturnType<typeof sshHostGroupDALFactory>;
|
||||
|
||||
export const sshHostGroupDALFactory = (db: TDbClient) => {
|
||||
const sshHostGroupOrm = ormify(db, TableName.SshHostGroup);
|
||||
|
||||
const findSshHostGroupsWithLoginMappings = async (projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
// First, get all the SSH host groups with their login mappings
|
||||
const rows = await (tx || db.replicaNode())(TableName.SshHostGroup)
|
||||
.leftJoin(
|
||||
TableName.SshHostLoginUser,
|
||||
`${TableName.SshHostGroup}.id`,
|
||||
`${TableName.SshHostLoginUser}.sshHostGroupId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SshHostLoginUserMapping,
|
||||
`${TableName.SshHostLoginUser}.id`,
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||
.where(`${TableName.SshHostGroup}.projectId`, projectId)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SshHostGroup).as("sshHostGroupId"),
|
||||
db.ref("projectId").withSchema(TableName.SshHostGroup),
|
||||
db.ref("name").withSchema(TableName.SshHostGroup),
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping)
|
||||
)
|
||||
.orderBy(`${TableName.SshHostGroup}.updatedAt`, "desc");
|
||||
|
||||
const hostsGrouped = groupBy(rows, (r) => r.sshHostGroupId);
|
||||
|
||||
const hostGroupIds = Object.keys(hostsGrouped);
|
||||
|
||||
type HostCountRow = {
|
||||
sshHostGroupId: string;
|
||||
host_count: string;
|
||||
};
|
||||
|
||||
const hostCountsQuery = (await (tx ||
|
||||
db
|
||||
.replicaNode()(TableName.SshHostGroupMembership)
|
||||
.select(`${TableName.SshHostGroupMembership}.sshHostGroupId`, db.raw(`count(*) as host_count`))
|
||||
.whereIn(`${TableName.SshHostGroupMembership}.sshHostGroupId`, hostGroupIds)
|
||||
.groupBy(`${TableName.SshHostGroupMembership}.sshHostGroupId`))) as HostCountRow[];
|
||||
|
||||
const hostCountsMap = hostCountsQuery.reduce<Record<string, number>>((acc, { sshHostGroupId, host_count }) => {
|
||||
acc[sshHostGroupId] = Number(host_count);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return Object.values(hostsGrouped).map((hostRows) => {
|
||||
const { sshHostGroupId, name } = hostRows[0];
|
||||
const loginMappingGrouped = groupBy(
|
||||
hostRows.filter((r) => r.loginUser),
|
||||
(r) => r.loginUser
|
||||
);
|
||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||
}
|
||||
}));
|
||||
return {
|
||||
id: sshHostGroupId,
|
||||
projectId,
|
||||
name,
|
||||
loginMappings,
|
||||
hostCount: hostCountsMap[sshHostGroupId] ?? 0
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.SshHostGroup}: FindSshHostGroupsWithLoginMappings` });
|
||||
}
|
||||
};
|
||||
|
||||
const findSshHostGroupByIdWithLoginMappings = async (sshHostGroupId: string, tx?: Knex) => {
|
||||
try {
|
||||
const rows = await (tx || db.replicaNode())(TableName.SshHostGroup)
|
||||
.leftJoin(
|
||||
TableName.SshHostLoginUser,
|
||||
`${TableName.SshHostGroup}.id`,
|
||||
`${TableName.SshHostLoginUser}.sshHostGroupId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SshHostLoginUserMapping,
|
||||
`${TableName.SshHostLoginUser}.id`,
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||
.where(`${TableName.SshHostGroup}.id`, sshHostGroupId)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SshHostGroup).as("sshHostGroupId"),
|
||||
db.ref("projectId").withSchema(TableName.SshHostGroup),
|
||||
db.ref("name").withSchema(TableName.SshHostGroup),
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping)
|
||||
);
|
||||
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
const { sshHostGroupId: id, projectId, name } = rows[0];
|
||||
|
||||
const loginMappingGrouped = groupBy(
|
||||
rows.filter((r) => r.loginUser),
|
||||
(r) => r.loginUser
|
||||
);
|
||||
|
||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||
}
|
||||
}));
|
||||
|
||||
return {
|
||||
id,
|
||||
projectId,
|
||||
name,
|
||||
loginMappings
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.SshHostGroup}: FindSshHostGroupByIdWithLoginMappings` });
|
||||
}
|
||||
};
|
||||
|
||||
const findAllSshHostsInGroup = async ({
|
||||
sshHostGroupId,
|
||||
offset = 0,
|
||||
limit,
|
||||
filter
|
||||
}: {
|
||||
sshHostGroupId: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
filter?: EHostGroupMembershipFilter;
|
||||
}) => {
|
||||
try {
|
||||
const sshHostGroup = await db
|
||||
.replicaNode()(TableName.SshHostGroup)
|
||||
.where(`${TableName.SshHostGroup}.id`, sshHostGroupId)
|
||||
.select("projectId")
|
||||
.first();
|
||||
|
||||
if (!sshHostGroup) {
|
||||
throw new BadRequestError({
|
||||
message: `SSH host group with ID ${sshHostGroupId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
const query = db
|
||||
.replicaNode()(TableName.SshHost)
|
||||
.where(`${TableName.SshHost}.projectId`, sshHostGroup.projectId)
|
||||
.leftJoin(TableName.SshHostGroupMembership, (bd) => {
|
||||
bd.on(`${TableName.SshHostGroupMembership}.sshHostId`, "=", `${TableName.SshHost}.id`).andOn(
|
||||
`${TableName.SshHostGroupMembership}.sshHostGroupId`,
|
||||
"=",
|
||||
db.raw("?", [sshHostGroupId])
|
||||
);
|
||||
})
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SshHost),
|
||||
db.ref("hostname").withSchema(TableName.SshHost),
|
||||
db.ref("alias").withSchema(TableName.SshHost),
|
||||
db.ref("sshHostGroupId").withSchema(TableName.SshHostGroupMembership),
|
||||
db.ref("createdAt").withSchema(TableName.SshHostGroupMembership).as("joinedGroupAt"),
|
||||
db.raw(`count(*) OVER() as total_count`)
|
||||
)
|
||||
.offset(offset)
|
||||
.orderBy(`${TableName.SshHost}.hostname`, "asc");
|
||||
|
||||
if (limit) {
|
||||
void query.limit(limit);
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
switch (filter) {
|
||||
case EHostGroupMembershipFilter.GROUP_MEMBERS:
|
||||
void query.andWhere(`${TableName.SshHostGroupMembership}.createdAt`, "is not", null);
|
||||
break;
|
||||
case EHostGroupMembershipFilter.NON_GROUP_MEMBERS:
|
||||
void query.andWhere(`${TableName.SshHostGroupMembership}.createdAt`, "is", null);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const hosts = await query;
|
||||
|
||||
return {
|
||||
hosts: hosts.map(({ id, hostname, alias, sshHostGroupId: memberGroupId, joinedGroupAt }) => ({
|
||||
id,
|
||||
hostname,
|
||||
alias,
|
||||
isPartOfGroup: !!memberGroupId,
|
||||
joinedGroupAt
|
||||
})),
|
||||
// @ts-expect-error col select is raw and not strongly typed
|
||||
totalCount: Number(hosts?.[0]?.total_count ?? 0)
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.SshHostGroupMembership}: FindAllSshHostsInGroup` });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
findSshHostGroupsWithLoginMappings,
|
||||
findSshHostGroupByIdWithLoginMappings,
|
||||
findAllSshHostsInGroup,
|
||||
...sshHostGroupOrm
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TSshHostGroupMembershipDALFactory = ReturnType<typeof sshHostGroupMembershipDALFactory>;
|
||||
|
||||
export const sshHostGroupMembershipDALFactory = (db: TDbClient) => {
|
||||
const sshHostGroupMembershipOrm = ormify(db, TableName.SshHostGroupMembership);
|
||||
|
||||
return {
|
||||
...sshHostGroupMembershipOrm
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { SshHostGroupsSchema } from "@app/db/schemas";
|
||||
|
||||
export const sanitizedSshHostGroup = SshHostGroupsSchema.pick({
|
||||
id: true,
|
||||
projectId: true,
|
||||
name: true
|
||||
});
|
||||
@@ -0,0 +1,397 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
|
||||
import { TSshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal";
|
||||
import { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
|
||||
import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal";
|
||||
import { TSshHostGroupMembershipDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-membership-dal";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { createSshLoginMappings } from "../ssh-host/ssh-host-fns";
|
||||
import {
|
||||
TAddHostToSshHostGroupDTO,
|
||||
TCreateSshHostGroupDTO,
|
||||
TDeleteSshHostGroupDTO,
|
||||
TGetSshHostGroupDTO,
|
||||
TListSshHostGroupHostsDTO,
|
||||
TRemoveHostFromSshHostGroupDTO,
|
||||
TUpdateSshHostGroupDTO
|
||||
} from "./ssh-host-group-types";
|
||||
|
||||
type TSshHostGroupServiceFactoryDep = {
|
||||
projectDAL: Pick<TProjectDALFactory, "findById" | "find">;
|
||||
sshHostDAL: Pick<TSshHostDALFactory, "findSshHostByIdWithLoginMappings">;
|
||||
sshHostGroupDAL: Pick<
|
||||
TSshHostGroupDALFactory,
|
||||
| "create"
|
||||
| "updateById"
|
||||
| "findById"
|
||||
| "deleteById"
|
||||
| "transaction"
|
||||
| "findSshHostGroupByIdWithLoginMappings"
|
||||
| "findAllSshHostsInGroup"
|
||||
| "findOne"
|
||||
| "find"
|
||||
>;
|
||||
sshHostGroupMembershipDAL: Pick<TSshHostGroupMembershipDALFactory, "create" | "deleteById" | "findOne">;
|
||||
sshHostLoginUserDAL: Pick<TSshHostLoginUserDALFactory, "create" | "transaction" | "delete">;
|
||||
sshHostLoginUserMappingDAL: Pick<TSshHostLoginUserMappingDALFactory, "insertMany">;
|
||||
userDAL: Pick<TUserDALFactory, "find">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TSshHostGroupServiceFactory = ReturnType<typeof sshHostGroupServiceFactory>;
|
||||
|
||||
export const sshHostGroupServiceFactory = ({
|
||||
projectDAL,
|
||||
sshHostDAL,
|
||||
sshHostGroupDAL,
|
||||
sshHostGroupMembershipDAL,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
}: TSshHostGroupServiceFactoryDep) => {
|
||||
const createSshHostGroup = async ({
|
||||
projectId,
|
||||
name,
|
||||
loginMappings,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TCreateSshHostGroupDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.SshHostGroups);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.sshHostGroups)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create SSH host group due to plan restriction. Upgrade plan to create group."
|
||||
});
|
||||
|
||||
const newSshHostGroup = await sshHostGroupDAL.transaction(async (tx) => {
|
||||
// (dangtony98): room to optimize check to ensure that
|
||||
// the SSH host group name is unique across the whole org
|
||||
const project = await projectDAL.findById(projectId, tx);
|
||||
if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` });
|
||||
const projects = await projectDAL.find(
|
||||
{
|
||||
orgId: project.orgId
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const existingSshHostGroup = await sshHostGroupDAL.find(
|
||||
{
|
||||
name,
|
||||
$in: {
|
||||
projectId: projects.map((p) => p.id)
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
if (existingSshHostGroup.length) {
|
||||
throw new BadRequestError({
|
||||
message: `SSH host group with name '${name}' already exists in the organization`
|
||||
});
|
||||
}
|
||||
|
||||
const sshHostGroup = await sshHostGroupDAL.create(
|
||||
{
|
||||
projectId,
|
||||
name
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await createSshLoginMappings({
|
||||
sshHostGroupId: sshHostGroup.id,
|
||||
loginMappings,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
tx
|
||||
});
|
||||
|
||||
const newSshHostGroupWithLoginMappings = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(
|
||||
sshHostGroup.id,
|
||||
tx
|
||||
);
|
||||
if (!newSshHostGroupWithLoginMappings) {
|
||||
throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroup.id}' not found` });
|
||||
}
|
||||
|
||||
return newSshHostGroupWithLoginMappings;
|
||||
});
|
||||
|
||||
return newSshHostGroup;
|
||||
};
|
||||
|
||||
const updateSshHostGroup = async ({
|
||||
sshHostGroupId,
|
||||
name,
|
||||
loginMappings,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TUpdateSshHostGroupDTO) => {
|
||||
const sshHostGroup = await sshHostGroupDAL.findById(sshHostGroupId);
|
||||
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: sshHostGroup.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SshHostGroups);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.sshHostGroups)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update SSH host group due to plan restriction. Upgrade plan to update group."
|
||||
});
|
||||
|
||||
const updatedSshHostGroup = await sshHostGroupDAL.transaction(async (tx) => {
|
||||
await sshHostGroupDAL.updateById(
|
||||
sshHostGroupId,
|
||||
{
|
||||
name
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (loginMappings) {
|
||||
await sshHostLoginUserDAL.delete({ sshHostGroupId: sshHostGroup.id }, tx);
|
||||
if (loginMappings.length) {
|
||||
await createSshLoginMappings({
|
||||
sshHostGroupId: sshHostGroup.id,
|
||||
loginMappings,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
projectId: sshHostGroup.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
tx
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updatedSshHostGroupWithLoginMappings = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(
|
||||
sshHostGroup.id,
|
||||
tx
|
||||
);
|
||||
if (!updatedSshHostGroupWithLoginMappings) {
|
||||
throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroup.id}' not found` });
|
||||
}
|
||||
|
||||
return updatedSshHostGroupWithLoginMappings;
|
||||
});
|
||||
|
||||
return updatedSshHostGroup;
|
||||
};
|
||||
|
||||
const getSshHostGroup = async ({
|
||||
sshHostGroupId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TGetSshHostGroupDTO) => {
|
||||
const sshHostGroup = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(sshHostGroupId);
|
||||
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: sshHostGroup.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHostGroups);
|
||||
|
||||
return sshHostGroup;
|
||||
};
|
||||
|
||||
const deleteSshHostGroup = async ({
|
||||
sshHostGroupId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TDeleteSshHostGroupDTO) => {
|
||||
const sshHostGroup = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(sshHostGroupId);
|
||||
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: sshHostGroup.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.SshHostGroups);
|
||||
|
||||
await sshHostGroupDAL.deleteById(sshHostGroupId);
|
||||
|
||||
return sshHostGroup;
|
||||
};
|
||||
|
||||
const listSshHostGroupHosts = async ({
|
||||
sshHostGroupId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
filter
|
||||
}: TListSshHostGroupHostsDTO) => {
|
||||
const sshHostGroup = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(sshHostGroupId);
|
||||
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: sshHostGroup.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHostGroups);
|
||||
|
||||
const { hosts, totalCount } = await sshHostGroupDAL.findAllSshHostsInGroup({ sshHostGroupId, filter });
|
||||
return { sshHostGroup, hosts, totalCount };
|
||||
};
|
||||
|
||||
const addHostToSshHostGroup = async ({
|
||||
sshHostGroupId,
|
||||
hostId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TAddHostToSshHostGroupDTO) => {
|
||||
const sshHostGroup = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(sshHostGroupId);
|
||||
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||
|
||||
const sshHost = await sshHostDAL.findSshHostByIdWithLoginMappings(hostId);
|
||||
if (!sshHost) {
|
||||
throw new NotFoundError({
|
||||
message: `SSH host with ID ${hostId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
if (sshHostGroup.projectId !== sshHost.projectId) {
|
||||
throw new BadRequestError({
|
||||
message: `SSH host with ID ${hostId} not found in project ${sshHostGroup.projectId}`
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: sshHostGroup.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SshHostGroups);
|
||||
|
||||
await sshHostGroupMembershipDAL.create({ sshHostGroupId, sshHostId: hostId });
|
||||
|
||||
return { sshHostGroup, sshHost };
|
||||
};
|
||||
|
||||
const removeHostFromSshHostGroup = async ({
|
||||
sshHostGroupId,
|
||||
hostId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TRemoveHostFromSshHostGroupDTO) => {
|
||||
const sshHostGroup = await sshHostGroupDAL.findSshHostGroupByIdWithLoginMappings(sshHostGroupId);
|
||||
if (!sshHostGroup) throw new NotFoundError({ message: `SSH host group with ID '${sshHostGroupId}' not found` });
|
||||
|
||||
const sshHost = await sshHostDAL.findSshHostByIdWithLoginMappings(hostId);
|
||||
if (!sshHost) {
|
||||
throw new NotFoundError({
|
||||
message: `SSH host with ID ${hostId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
if (sshHostGroup.projectId !== sshHost.projectId) {
|
||||
throw new BadRequestError({
|
||||
message: `SSH host with ID ${hostId} not found in project ${sshHostGroup.projectId}`
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: sshHostGroup.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SshHostGroups);
|
||||
|
||||
const sshHostGroupMembership = await sshHostGroupMembershipDAL.findOne({
|
||||
sshHostGroupId,
|
||||
sshHostId: hostId
|
||||
});
|
||||
|
||||
if (!sshHostGroupMembership) {
|
||||
throw new NotFoundError({
|
||||
message: `SSH host with ID ${hostId} not found in SSH host group with ID ${sshHostGroupId}`
|
||||
});
|
||||
}
|
||||
|
||||
await sshHostGroupMembershipDAL.deleteById(sshHostGroupMembership.id);
|
||||
|
||||
return { sshHostGroup, sshHost };
|
||||
};
|
||||
|
||||
return {
|
||||
createSshHostGroup,
|
||||
getSshHostGroup,
|
||||
deleteSshHostGroup,
|
||||
updateSshHostGroup,
|
||||
listSshHostGroupHosts,
|
||||
addHostToSshHostGroup,
|
||||
removeHostFromSshHostGroup
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { TLoginMapping } from "@app/ee/services/ssh-host/ssh-host-types";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreateSshHostGroupDTO = {
|
||||
name: string;
|
||||
loginMappings: TLoginMapping[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TUpdateSshHostGroupDTO = {
|
||||
sshHostGroupId: string;
|
||||
name?: string;
|
||||
loginMappings?: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetSshHostGroupDTO = {
|
||||
sshHostGroupId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteSshHostGroupDTO = {
|
||||
sshHostGroupId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListSshHostGroupHostsDTO = {
|
||||
sshHostGroupId: string;
|
||||
filter?: EHostGroupMembershipFilter;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TAddHostToSshHostGroupDTO = {
|
||||
sshHostGroupId: string;
|
||||
hostId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TRemoveHostFromSshHostGroupDTO = {
|
||||
sshHostGroupId: string;
|
||||
hostId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export enum EHostGroupMembershipFilter {
|
||||
GROUP_MEMBERS = "group-members",
|
||||
NON_GROUP_MEMBERS = "non-group-members"
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import { DatabaseError } from "@app/lib/errors";
|
||||
import { groupBy, unique } from "@app/lib/fn";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
import { LoginMappingSource } from "./ssh-host-types";
|
||||
|
||||
export type TSshHostDALFactory = ReturnType<typeof sshHostDALFactory>;
|
||||
|
||||
export const sshHostDALFactory = (db: TDbClient) => {
|
||||
@@ -13,20 +15,22 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findUserAccessibleSshHosts = async (projectIds: string[], userId: string, tx?: Knex) => {
|
||||
try {
|
||||
const user = await (tx || db.replicaNode())(TableName.Users).where({ id: userId }).select("username").first();
|
||||
const knex = tx || db.replicaNode();
|
||||
|
||||
const user = await knex(TableName.Users).where({ id: userId }).select("username").first();
|
||||
|
||||
if (!user) {
|
||||
throw new DatabaseError({ name: `${TableName.Users}: UserNotFound`, error: new Error("User not found") });
|
||||
}
|
||||
|
||||
const rows = await (tx || db.replicaNode())(TableName.SshHost)
|
||||
// get hosts where user has direct login mappings
|
||||
const directHostRows = await knex(TableName.SshHost)
|
||||
.leftJoin(TableName.SshHostLoginUser, `${TableName.SshHost}.id`, `${TableName.SshHostLoginUser}.sshHostId`)
|
||||
.leftJoin(
|
||||
TableName.SshHostLoginUserMapping,
|
||||
`${TableName.SshHostLoginUser}.id`,
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SshHostLoginUserMapping}.userId`)
|
||||
.whereIn(`${TableName.SshHost}.projectId`, projectIds)
|
||||
.andWhere(`${TableName.SshHostLoginUserMapping}.userId`, userId)
|
||||
.select(
|
||||
@@ -37,26 +41,70 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
||||
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||
)
|
||||
.orderBy(`${TableName.SshHost}.updatedAt`, "desc");
|
||||
);
|
||||
|
||||
const grouped = groupBy(rows, (r) => r.sshHostId);
|
||||
return Object.values(grouped).map((hostRows) => {
|
||||
// get hosts where user has login mappings via host groups
|
||||
const groupHostRows = await knex(TableName.SshHostGroupMembership)
|
||||
.join(
|
||||
TableName.SshHostLoginUser,
|
||||
`${TableName.SshHostGroupMembership}.sshHostGroupId`,
|
||||
`${TableName.SshHostLoginUser}.sshHostGroupId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SshHostLoginUserMapping,
|
||||
`${TableName.SshHostLoginUser}.id`,
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.join(TableName.SshHost, `${TableName.SshHostGroupMembership}.sshHostId`, `${TableName.SshHost}.id`)
|
||||
.whereIn(`${TableName.SshHost}.projectId`, projectIds)
|
||||
.andWhere(`${TableName.SshHostLoginUserMapping}.userId`, userId)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||
db.ref("projectId").withSchema(TableName.SshHost),
|
||||
db.ref("hostname").withSchema(TableName.SshHost),
|
||||
db.ref("alias").withSchema(TableName.SshHost),
|
||||
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
||||
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||
);
|
||||
|
||||
const directHostRowsWithSource = directHostRows.map((row) => ({
|
||||
...row,
|
||||
source: LoginMappingSource.HOST
|
||||
}));
|
||||
|
||||
const groupHostRowsWithSource = groupHostRows.map((row) => ({
|
||||
...row,
|
||||
source: LoginMappingSource.HOST_GROUP
|
||||
}));
|
||||
|
||||
const mergedRows = [...directHostRowsWithSource, ...groupHostRowsWithSource];
|
||||
|
||||
const hostsGrouped = groupBy(mergedRows, (r) => r.sshHostId);
|
||||
|
||||
return Object.values(hostsGrouped).map((hostRows) => {
|
||||
const { sshHostId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } =
|
||||
hostRows[0];
|
||||
|
||||
const loginMappingGrouped = groupBy(hostRows, (r) => r.loginUser);
|
||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, mappings]) => {
|
||||
// Prefer HOST source over HOST_GROUP
|
||||
const preferredMapping =
|
||||
mappings.find((m) => m.source === LoginMappingSource.HOST) ||
|
||||
mappings.find((m) => m.source === LoginMappingSource.HOST_GROUP);
|
||||
|
||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: [user.username]
|
||||
}
|
||||
}));
|
||||
return {
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: [user.username]
|
||||
},
|
||||
source: preferredMapping!.source
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: sshHostId,
|
||||
@@ -101,20 +149,57 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
)
|
||||
.orderBy(`${TableName.SshHost}.updatedAt`, "desc");
|
||||
|
||||
// process login mappings inherited from groups that hosts are part of
|
||||
const hostIds = unique(rows.map((r) => r.sshHostId)).filter(Boolean);
|
||||
const groupRows = await (tx || db.replicaNode())(TableName.SshHostGroupMembership)
|
||||
.join(
|
||||
TableName.SshHostLoginUser,
|
||||
`${TableName.SshHostGroupMembership}.sshHostGroupId`,
|
||||
`${TableName.SshHostLoginUser}.sshHostGroupId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SshHostLoginUserMapping,
|
||||
`${TableName.SshHostLoginUser}.id`,
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||
.select(
|
||||
db.ref("sshHostId").withSchema(TableName.SshHostGroupMembership),
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
db.ref("username").withSchema(TableName.Users)
|
||||
)
|
||||
.whereIn(`${TableName.SshHostGroupMembership}.sshHostId`, hostIds);
|
||||
|
||||
const groupedGroupMappings = groupBy(groupRows, (r) => r.sshHostId);
|
||||
|
||||
const hostsGrouped = groupBy(rows, (r) => r.sshHostId);
|
||||
return Object.values(hostsGrouped).map((hostRows) => {
|
||||
const { sshHostId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = hostRows[0];
|
||||
|
||||
// direct login mappings
|
||||
const loginMappingGrouped = groupBy(
|
||||
hostRows.filter((r) => r.loginUser),
|
||||
(r) => r.loginUser
|
||||
);
|
||||
|
||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||
const directMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||
}
|
||||
},
|
||||
source: LoginMappingSource.HOST
|
||||
}));
|
||||
|
||||
// group-inherited login mappings
|
||||
const inheritedGroupRows = groupedGroupMappings[sshHostId] || [];
|
||||
const inheritedGrouped = groupBy(inheritedGroupRows, (r) => r.loginUser);
|
||||
|
||||
const groupMappings = Object.entries(inheritedGrouped).map(([loginUser, entries]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||
},
|
||||
source: LoginMappingSource.HOST_GROUP
|
||||
}));
|
||||
|
||||
return {
|
||||
@@ -124,7 +209,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
projectId,
|
||||
userCertTtl,
|
||||
hostCertTtl,
|
||||
loginMappings,
|
||||
loginMappings: [...directMappings, ...groupMappings],
|
||||
userSshCaId,
|
||||
hostSshCaId
|
||||
};
|
||||
@@ -163,16 +248,50 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
|
||||
const { sshHostId: id, projectId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = rows[0];
|
||||
|
||||
const loginMappingGrouped = groupBy(
|
||||
// direct login mappings
|
||||
const directGrouped = groupBy(
|
||||
rows.filter((r) => r.loginUser),
|
||||
(r) => r.loginUser
|
||||
);
|
||||
|
||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||
const directMappings = Object.entries(directGrouped).map(([loginUser, entries]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||
}
|
||||
},
|
||||
source: LoginMappingSource.HOST
|
||||
}));
|
||||
|
||||
// group login mappings
|
||||
const groupRows = await (tx || db.replicaNode())(TableName.SshHostGroupMembership)
|
||||
.join(
|
||||
TableName.SshHostLoginUser,
|
||||
`${TableName.SshHostGroupMembership}.sshHostGroupId`,
|
||||
`${TableName.SshHostLoginUser}.sshHostGroupId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SshHostLoginUserMapping,
|
||||
`${TableName.SshHostLoginUser}.id`,
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||
.where(`${TableName.SshHostGroupMembership}.sshHostId`, sshHostId)
|
||||
.select(
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
db.ref("username").withSchema(TableName.Users)
|
||||
);
|
||||
|
||||
const groupGrouped = groupBy(
|
||||
groupRows.filter((r) => r.loginUser),
|
||||
(r) => r.loginUser
|
||||
);
|
||||
|
||||
const groupMappings = Object.entries(groupGrouped).map(([loginUser, entries]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||
},
|
||||
source: LoginMappingSource.HOST_GROUP
|
||||
}));
|
||||
|
||||
return {
|
||||
@@ -182,7 +301,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
alias,
|
||||
userCertTtl,
|
||||
hostCertTtl,
|
||||
loginMappings,
|
||||
loginMappings: [...directMappings, ...groupMappings],
|
||||
userSshCaId,
|
||||
hostSshCaId
|
||||
};
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TCreateSshLoginMappingsDTO } from "./ssh-host-types";
|
||||
|
||||
/**
|
||||
* Create SSH login mappings for a given SSH host
|
||||
* or SSH host group.
|
||||
*/
|
||||
export const createSshLoginMappings = async ({
|
||||
sshHostId,
|
||||
sshHostGroupId,
|
||||
loginMappings,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
tx: outerTx
|
||||
}: TCreateSshLoginMappingsDTO) => {
|
||||
const processCreation = async (tx: Knex) => {
|
||||
// (dangtony98): room to optimize
|
||||
for await (const { loginUser, allowedPrincipals } of loginMappings) {
|
||||
const sshHostLoginUser = await sshHostLoginUserDAL.create(
|
||||
// (dangtony98): should either pass in sshHostId or sshHostGroupId but not both
|
||||
{
|
||||
sshHostId,
|
||||
sshHostGroupId,
|
||||
loginUser
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (allowedPrincipals.usernames.length > 0) {
|
||||
const users = await userDAL.find(
|
||||
{
|
||||
$in: {
|
||||
username: allowedPrincipals.usernames
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const foundUsernames = new Set(users.map((u) => u.username));
|
||||
|
||||
for (const uname of allowedPrincipals.usernames) {
|
||||
if (!foundUsernames.has(uname)) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid username: ${uname}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for await (const user of users) {
|
||||
// check that each user has access to the SSH project
|
||||
await permissionService.getUserProjectPermission({
|
||||
userId: user.id,
|
||||
projectId,
|
||||
authMethod: actorAuthMethod,
|
||||
userOrgId: actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
}
|
||||
|
||||
await sshHostLoginUserMappingDAL.insertMany(
|
||||
users.map((user) => ({
|
||||
sshHostLoginUserId: sshHostLoginUser.id,
|
||||
userId: user.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (outerTx) {
|
||||
return processCreation(outerTx);
|
||||
}
|
||||
|
||||
return sshHostLoginUserDAL.transaction(processCreation);
|
||||
};
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
getSshPublicKey
|
||||
} from "../ssh/ssh-certificate-authority-fns";
|
||||
import { SshCertType } from "../ssh/ssh-certificate-authority-types";
|
||||
import { createSshLoginMappings } from "./ssh-host-fns";
|
||||
import {
|
||||
TCreateSshHostDTO,
|
||||
TDeleteSshHostDTO,
|
||||
@@ -202,56 +203,18 @@ export const sshHostServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
|
||||
// (dangtony98): room to optimize
|
||||
for await (const { loginUser, allowedPrincipals } of loginMappings) {
|
||||
const sshHostLoginUser = await sshHostLoginUserDAL.create(
|
||||
{
|
||||
sshHostId: host.id,
|
||||
loginUser
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (allowedPrincipals.usernames.length > 0) {
|
||||
const users = await userDAL.find(
|
||||
{
|
||||
$in: {
|
||||
username: allowedPrincipals.usernames
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const foundUsernames = new Set(users.map((u) => u.username));
|
||||
|
||||
for (const uname of allowedPrincipals.usernames) {
|
||||
if (!foundUsernames.has(uname)) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid username: ${uname}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for await (const user of users) {
|
||||
// check that each user has access to the SSH project
|
||||
await permissionService.getUserProjectPermission({
|
||||
userId: user.id,
|
||||
projectId,
|
||||
authMethod: actorAuthMethod,
|
||||
userOrgId: actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
}
|
||||
|
||||
await sshHostLoginUserMappingDAL.insertMany(
|
||||
users.map((user) => ({
|
||||
sshHostLoginUserId: sshHostLoginUser.id,
|
||||
userId: user.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
await createSshLoginMappings({
|
||||
sshHostId: host.id,
|
||||
loginMappings,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
tx
|
||||
});
|
||||
|
||||
const newSshHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(host.id, tx);
|
||||
if (!newSshHostWithLoginMappings) {
|
||||
@@ -310,54 +273,18 @@ export const sshHostServiceFactory = ({
|
||||
if (loginMappings) {
|
||||
await sshHostLoginUserDAL.delete({ sshHostId: host.id }, tx);
|
||||
if (loginMappings.length) {
|
||||
for await (const { loginUser, allowedPrincipals } of loginMappings) {
|
||||
const sshHostLoginUser = await sshHostLoginUserDAL.create(
|
||||
{
|
||||
sshHostId: host.id,
|
||||
loginUser
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (allowedPrincipals.usernames.length > 0) {
|
||||
const users = await userDAL.find(
|
||||
{
|
||||
$in: {
|
||||
username: allowedPrincipals.usernames
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const foundUsernames = new Set(users.map((u) => u.username));
|
||||
|
||||
for (const uname of allowedPrincipals.usernames) {
|
||||
if (!foundUsernames.has(uname)) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid username: ${uname}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for await (const user of users) {
|
||||
await permissionService.getUserProjectPermission({
|
||||
userId: user.id,
|
||||
projectId: host.projectId,
|
||||
authMethod: actorAuthMethod,
|
||||
userOrgId: actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
}
|
||||
|
||||
await sshHostLoginUserMappingDAL.insertMany(
|
||||
users.map((user) => ({
|
||||
sshHostLoginUserId: sshHostLoginUser.id,
|
||||
userId: user.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
await createSshLoginMappings({
|
||||
sshHostId: host.id,
|
||||
loginMappings,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
projectId: host.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
tx
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { TSshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal";
|
||||
import { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
export type TListSshHostsDTO = Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TLoginMapping = {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export enum LoginMappingSource {
|
||||
HOST = "host",
|
||||
HOST_GROUP = "hostGroup"
|
||||
}
|
||||
|
||||
export type TCreateSshHostDTO = {
|
||||
hostname: string;
|
||||
alias?: string;
|
||||
userCertTtl: string;
|
||||
hostCertTtl: string;
|
||||
loginMappings: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
loginMappings: TLoginMapping[];
|
||||
userSshCaId?: string;
|
||||
hostSshCaId?: string;
|
||||
} & TProjectPermission;
|
||||
@@ -23,12 +37,7 @@ export type TUpdateSshHostDTO = {
|
||||
alias?: string;
|
||||
userCertTtl?: string;
|
||||
hostCertTtl?: string;
|
||||
loginMappings?: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
loginMappings?: TLoginMapping[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetSshHostDTO = {
|
||||
@@ -48,3 +57,19 @@ export type TIssueSshHostHostCertDTO = {
|
||||
sshHostId: string;
|
||||
publicKey: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
type BaseCreateSshLoginMappingsDTO = {
|
||||
loginMappings: TLoginMapping[];
|
||||
sshHostLoginUserDAL: Pick<TSshHostLoginUserDALFactory, "create" | "transaction">;
|
||||
sshHostLoginUserMappingDAL: Pick<TSshHostLoginUserMappingDALFactory, "insertMany">;
|
||||
userDAL: Pick<TUserDALFactory, "find">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getUserProjectPermission">;
|
||||
projectId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
tx?: Knex;
|
||||
};
|
||||
|
||||
export type TCreateSshLoginMappingsDTO =
|
||||
| (BaseCreateSshLoginMappingsDTO & { sshHostId: string; sshHostGroupId?: undefined })
|
||||
| (BaseCreateSshLoginMappingsDTO & { sshHostGroupId: string; sshHostId?: undefined });
|
||||
|
||||
@@ -282,7 +282,7 @@ export const sshCertificateAuthorityServiceFactory = ({
|
||||
|
||||
// set [keyId] depending on if [allowCustomKeyIds] is true or false
|
||||
const keyId = sshCertificateTemplate.allowCustomKeyIds
|
||||
? requestedKeyId ?? `${actor}-${actorId}`
|
||||
? (requestedKeyId ?? `${actor}-${actorId}`)
|
||||
: `${actor}-${actorId}`;
|
||||
|
||||
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });
|
||||
@@ -404,7 +404,7 @@ export const sshCertificateAuthorityServiceFactory = ({
|
||||
|
||||
// set [keyId] depending on if [allowCustomKeyIds] is true or false
|
||||
const keyId = sshCertificateTemplate.allowCustomKeyIds
|
||||
? requestedKeyId ?? `${actor}-${actorId}`
|
||||
? (requestedKeyId ?? `${actor}-${actorId}`)
|
||||
: `${actor}-${actorId}`;
|
||||
|
||||
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });
|
||||
|
||||
Reference in New Issue
Block a user