diff --git a/backend/src/ee/routes/v1/ssh-host-router.ts b/backend/src/ee/routes/v1/ssh-host-router.ts index 15ec5d417..e129903cd 100644 --- a/backend/src/ee/routes/v1/ssh-host-router.ts +++ b/backend/src/ee/routes/v1/ssh-host-router.ts @@ -10,6 +10,51 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerSshHostRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.array( + sanitizedSshHost.extend({ + loginMappings: z.array( + z.object({ + loginUser: z.string(), + allowedPrincipals: z.array(z.string()) + }) + ) + }) + ) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const hosts = await server.services.sshHost.listSshHosts({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + // TODO: audit log + // await server.services.auditLog.createAuditLog({ + // ...req.auditLogInfo, + // projectId: certificateTemplate.projectId, + // event: { + // type: EventType.GET_SSH_CERTIFICATE_TEMPLATE, + // metadata: { + // certificateTemplateId: certificateTemplate.id + // } + // } + // }); + + return hosts; + } + }); + server.route({ method: "GET", url: "/:sshHostId", @@ -91,7 +136,9 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => { }) .array() .default([]) - .describe(SSH_HOSTS.CREATE.loginMappings) + .describe(SSH_HOSTS.CREATE.loginMappings), + userSshCaId: z.string().describe(SSH_HOSTS.CREATE.userSshCaId).optional(), + hostSshCaId: z.string().describe(SSH_HOSTS.CREATE.hostSshCaId).optional() }), response: { 200: sanitizedSshHost.extend({ @@ -265,7 +312,6 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => { }); server.route({ - // TODO: consider just using the SSH issue creds endpoint method: "POST", url: "/:sshHostId/issue", config: { @@ -287,15 +333,18 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => { }) } }, - handler: () => { - // const { serialNumber, signedPublicKey, privateKey, publicKey, certificateTemplate, ttl, keyId } = - // await server.services.sshCertificateAuthority.issueSshCreds({ - // actor: req.permission.type, - // actorId: req.permission.id, - // actorAuthMethod: req.permission.authMethod, - // actorOrgId: req.permission.orgId, - // ...req.body - // }); + handler: async (req) => { + const { serialNumber, signedPublicKey, privateKey, publicKey, keyAlgorithm } = + await server.services.sshHost.issueSshCredsFromHost({ + sshHostId: req.params.sshHostId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + // TODO: add audit log + // await server.services.auditLog.createAuditLog({ // ...req.auditLogInfo, // orgId: req.permission.orgId, @@ -320,19 +369,13 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => { // ...req.auditLogInfo // } // }); - // return { - // serialNumber, - // signedKey: signedPublicKey, - // privateKey, - // publicKey, - // keyAlgorithm: req.body.keyAlgorithm - // }; + return { - serialNumber: "", - signedKey: "", - privateKey: "", - publicKey: "", - keyAlgorithm: SshCertKeyAlgorithm.ED25519 + serialNumber, + signedKey: signedPublicKey, + privateKey, + publicKey, + keyAlgorithm }; } }); diff --git a/backend/src/ee/services/ssh-host/ssh-host-dal.ts b/backend/src/ee/services/ssh-host/ssh-host-dal.ts index aeb180191..52505f18b 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-dal.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-dal.ts @@ -11,6 +11,69 @@ export type TSshHostDALFactory = ReturnType; export const sshHostDALFactory = (db: TDbClient) => { const sshHostOrm = ormify(db, TableName.SshHost); + const findSshHostsWithPrincipalsAcrossProjects = async (projectIds: string[], principals: string[], tx?: Knex) => { + try { + const matchingSshHosts = await (tx || db.replicaNode())(TableName.SshHost) + .leftJoin( + TableName.SshHostLoginMapping, + `${TableName.SshHost}.id`, + `${TableName.SshHostLoginMapping}.sshHostId` + ) + .whereIn(`${TableName.SshHost}.projectId`, projectIds) + .whereRaw(`"${TableName.SshHostLoginMapping}"."allowedPrincipals" && ?::text[]`, [principals]) + .select( + db.ref("id").withSchema(TableName.SshHost).as("sshHostId"), + db.ref("projectId").withSchema(TableName.SshHost), + db.ref("hostname").withSchema(TableName.SshHost), + db.ref("userCertTtl").withSchema(TableName.SshHost), + db.ref("hostCertTtl").withSchema(TableName.SshHost), + db.ref("loginUser").withSchema(TableName.SshHostLoginMapping), + db.ref("allowedPrincipals").withSchema(TableName.SshHostLoginMapping), + db.ref("userSshCaId").withSchema(TableName.SshHost), + db.ref("hostSshCaId").withSchema(TableName.SshHost) + ) + .orderBy(`${TableName.SshHost}.updatedAt`, "desc"); + + const grouped = groupBy(matchingSshHosts, (r) => r.sshHostId); + return Object.values(grouped).map((hostRows) => { + const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } = hostRows[0]; + + const loginMappingGrouped = groupBy( + hostRows.filter((r) => r.loginUser), + (r) => r.loginUser + ); + + const loginMappings = Object.entries(loginMappingGrouped) + .map(([loginUser, entries]) => { + const filteredPrincipals = unique(entries.flatMap((entry) => entry.allowedPrincipals ?? [])).filter( + (principal) => principals.includes(principal) + ); + + if (filteredPrincipals.length === 0) return null; + + return { + loginUser, + allowedPrincipals: filteredPrincipals + }; + }) + .filter(Boolean) as { loginUser: string; allowedPrincipals: string[] }[]; + + return { + id: sshHostId, + hostname, + projectId, + userCertTtl, + hostCertTtl, + loginMappings, + userSshCaId, + hostSshCaId + }; + }); + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostsWithPrincipalsAcrossProjects` }); + } + }; + const findSshHostsWithLoginMappings = async (projectId: string, tx?: Knex) => { try { const rows = await (tx || db.replicaNode())(TableName.SshHost) @@ -27,13 +90,15 @@ export const sshHostDALFactory = (db: TDbClient) => { db.ref("userCertTtl").withSchema(TableName.SshHost), db.ref("hostCertTtl").withSchema(TableName.SshHost), db.ref("loginUser").withSchema(TableName.SshHostLoginMapping), - db.ref("allowedPrincipals").withSchema(TableName.SshHostLoginMapping) + db.ref("allowedPrincipals").withSchema(TableName.SshHostLoginMapping), + db.ref("userSshCaId").withSchema(TableName.SshHost), + db.ref("hostSshCaId").withSchema(TableName.SshHost) ) .orderBy(`${TableName.SshHost}.updatedAt`, "desc"); const hostsGrouped = groupBy(rows, (r) => r.sshHostId); return Object.values(hostsGrouped).map((hostRows) => { - const { sshHostId, hostname, userCertTtl, hostCertTtl } = hostRows[0]; + const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = hostRows[0]; const loginMappingGrouped = groupBy( hostRows.filter((r) => r.loginUser), @@ -51,7 +116,9 @@ export const sshHostDALFactory = (db: TDbClient) => { hostname, userCertTtl, hostCertTtl, - loginMappings + loginMappings, + userSshCaId, + hostSshCaId }; }); } catch (error) { @@ -75,12 +142,14 @@ export const sshHostDALFactory = (db: TDbClient) => { db.ref("userCertTtl").withSchema(TableName.SshHost), db.ref("hostCertTtl").withSchema(TableName.SshHost), db.ref("loginUser").withSchema(TableName.SshHostLoginMapping), - db.ref("allowedPrincipals").withSchema(TableName.SshHostLoginMapping) + db.ref("allowedPrincipals").withSchema(TableName.SshHostLoginMapping), + db.ref("userSshCaId").withSchema(TableName.SshHost), + db.ref("hostSshCaId").withSchema(TableName.SshHost) ); if (rows.length === 0) return null; - const { sshHostId: id, projectId, hostname, userCertTtl, hostCertTtl } = rows[0]; + const { sshHostId: id, projectId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = rows[0]; const loginMappingGrouped = groupBy( rows.filter((r) => r.loginUser), @@ -98,7 +167,9 @@ export const sshHostDALFactory = (db: TDbClient) => { hostname, userCertTtl, hostCertTtl, - loginMappings + loginMappings, + userSshCaId, + hostSshCaId }; } catch (error) { throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostByIdWithLoginMappings` }); @@ -107,6 +178,7 @@ export const sshHostDALFactory = (db: TDbClient) => { return { ...sshHostOrm, + findSshHostsWithPrincipalsAcrossProjects, findSshHostsWithLoginMappings, findSshHostByIdWithLoginMappings }; diff --git a/backend/src/ee/services/ssh-host/ssh-host-service.ts b/backend/src/ee/services/ssh-host/ssh-host-service.ts index c6f7b565b..d02107870 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-service.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-service.ts @@ -1,24 +1,54 @@ import { ForbiddenError } from "@casl/ability"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, ProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; +import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal"; +import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types"; import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal"; import { TSshHostLoginMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-mapping-dal"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal"; +import { TUserDALFactory } from "@app/services/user/user-dal"; -import { TCreateSshHostDTO, TDeleteSshHostDTO, TGetSshHostDTO, TUpdateSshHostDTO } from "./ssh-host-types"; +import { convertActorToPrincipals, createSshCert, createSshKeyPair } from "../ssh/ssh-certificate-authority-fns"; +import { SshCertType } from "../ssh/ssh-certificate-authority-types"; +import { + TCreateSshHostDTO, + TDeleteSshHostDTO, + TGetSshHostDTO, + TIssueSshCredsFromHostDTO, + TListSshHostsDTO, + TUpdateSshHostDTO +} from "./ssh-host-types"; type TSshCertificateAuthorityServiceFactoryDep = { + userDAL: Pick; + projectDAL: Pick; + projectSshConfigDAL: Pick; + sshCertificateAuthorityDAL: Pick; + sshCertificateAuthoritySecretDAL: Pick; sshHostDAL: Pick< TSshHostDALFactory, - "transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" | "findSshHostByIdWithLoginMappings" + | "transaction" + | "create" + | "findById" + | "updateById" + | "deleteById" + | "findOne" + | "findSshHostByIdWithLoginMappings" + | "findSshHostsWithPrincipalsAcrossProjects" >; sshHostLoginMappingDAL: Pick< TSshHostLoginMappingDALFactory, "transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" | "insertMany" | "delete" >; permissionService: Pick; + kmsService: Pick; }; export type TSshHostServiceFactory = ReturnType; @@ -29,17 +59,72 @@ export type TSshHostServiceFactory = ReturnType; */ export const sshHostServiceFactory = ({ - projectDAL + userDAL, + projectDAL, + projectSshConfigDAL, + sshCertificateAuthorityDAL, + sshCertificateAuthoritySecretDAL, sshHostDAL, sshHostLoginMappingDAL, - permissionService + permissionService, + kmsService }: TSshCertificateAuthorityServiceFactoryDep) => { + /** + * Return list of all SSH hosts that a user has access to across all SSH projects in the organization + */ + const listSshHosts = async ({ actorId, actorAuthMethod, actor, actorOrgId }: TListSshHostsDTO) => { + const sshProjects = await projectDAL.find({ + orgId: actorOrgId, + type: ProjectType.SSH + }); + + const projectIdsWithAccess: string[] = []; + + for await (const project of sshProjects) { + let hasAccess = false; + + try { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: project.id, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + // TODO: consider glob-based permission items + hasAccess = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts); + } catch { + hasAccess = false; + } + + if (hasAccess) { + projectIdsWithAccess.push(project.id); + } + } + + // const principals = await convertActorToPrincipals({ + // actor, + // actorId, + // userDAL + // }); + + const hosts = await sshHostDAL.findSshHostsWithPrincipalsAcrossProjects(projectIdsWithAccess, [ + "dangtony98+2@gmail.com" // hardcode for now + ]); + + return hosts; + }; + const createSshHost = async ({ projectId, hostname, userCertTtl, hostCertTtl, loginMappings, + userSshCaId: requestedUserSshCaId, + hostSshCaId: requestedHostSshCaId, actorId, actorAuthMethod, actor, @@ -56,6 +141,42 @@ export const sshHostServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.SshHosts); + const resolveSshCaId = async ({ + requestedId, + fallbackId, + label + }: { + requestedId?: string; + fallbackId?: string | null; + label: "User" | "Host"; + }) => { + const finalId = requestedId ?? fallbackId; + if (!finalId) { + throw new BadRequestError({ message: `Missing ${label.toLowerCase()} SSH CA` }); + } + + const ca = await sshCertificateAuthorityDAL.findById(finalId); + if (!ca) { + throw new BadRequestError({ message: `${label} SSH CA with ID '${finalId}' not found` }); + } + + return ca.id; + }; + + const projectSshConfig = await projectSshConfigDAL.findOne({ projectId }); + + const userSshCaId = await resolveSshCaId({ + requestedId: requestedUserSshCaId, + fallbackId: projectSshConfig?.defaultUserSshCaId, + label: "User" + }); + + const hostSshCaId = await resolveSshCaId({ + requestedId: requestedHostSshCaId, + fallbackId: projectSshConfig?.defaultHostSshCaId, + label: "Host" + }); + const newSshHost = await sshHostDAL.transaction(async (tx) => { const existingHost = await sshHostDAL.findOne( { @@ -71,16 +192,14 @@ export const sshHostServiceFactory = ({ }); } - // attach hosts? - - // create host ssh cas in default bound to default user ca and host ca - const host = await sshHostDAL.create( { projectId, hostname, userCertTtl, - hostCertTtl + hostCertTtl, + userSshCaId, + hostSshCaId }, tx ); @@ -211,10 +330,88 @@ export const sshHostServiceFactory = ({ return host; }; + /** + * Return SSH certificate and corresponding new SSH public-private key pair where + * SSH public key is signed using CA behind SSH certificate with name [templateName]. + * + * Note: Used for issuing SSH credentials as part of request against a specific SSH Host. + */ + const issueSshCredsFromHost = async ({ + sshHostId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TIssueSshCredsFromHostDTO) => { + const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId); + if (!host) { + throw new NotFoundError({ + message: `SSH host with ID ${sshHostId} not found` + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: host.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts); + + // TODO: update permissions + + const keyId = `${actor}-${actorId}`; + + const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.userSshCaId }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: host.projectId + }); + + const decryptedCaPrivateKey = secretManagerDecryptor({ + cipherTextBlob: sshCaSecret.encryptedPrivateKey + }); + + // create user key pair + const keyAlgorithm = SshCertKeyAlgorithm.ED25519; // (dangtony98): will support more algorithms in the future + const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm); + + const principals = await convertActorToPrincipals({ + actor, + actorId, + userDAL + }); + + const { serialNumber, signedPublicKey, ttl } = await createSshCert({ + caPrivateKey: decryptedCaPrivateKey.toString("utf8"), + clientPublicKey: publicKey, + keyId, + principals, + requestedTtl: host.userCertTtl, + certType: SshCertType.USER + }); + + return { + serialNumber, + signedPublicKey, + privateKey, + publicKey, + ttl, + keyId, + keyAlgorithm + }; + }; + return { + listSshHosts, createSshHost, updateSshHost, deleteSshHost, - getSshHost + getSshHost, + issueSshCredsFromHost }; }; diff --git a/backend/src/ee/services/ssh-host/ssh-host-types.ts b/backend/src/ee/services/ssh-host/ssh-host-types.ts index df480cd6c..89b267947 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-types.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-types.ts @@ -1,5 +1,7 @@ import { TProjectPermission } from "@app/lib/types"; +export type TListSshHostsDTO = Omit; + export type TCreateSshHostDTO = { hostname: string; userCertTtl: string; @@ -8,6 +10,8 @@ export type TCreateSshHostDTO = { loginUser: string; allowedPrincipals: string[]; }[]; + userSshCaId?: string; + hostSshCaId?: string; } & TProjectPermission; export type TUpdateSshHostDTO = { @@ -28,3 +32,7 @@ export type TGetSshHostDTO = { export type TDeleteSshHostDTO = { sshHostId: string; } & Omit; + +export type TIssueSshCredsFromHostDTO = { + sshHostId: string; +} & Omit; diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts index 578e8b92b..bed600d21 100644 --- a/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts @@ -11,6 +11,7 @@ import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certif import { BadRequestError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; +import { ActorType } from "@app/services/auth/auth-type"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { @@ -21,6 +22,7 @@ import { SshCaKeySource, SshCaStatus, SshCertType, + TConvertActorToPrincipalsDTO, TCreateSshCaHelperDTO, TCreateSshCertDTO } from "./ssh-certificate-authority-types"; @@ -439,17 +441,32 @@ export const createSshCert = async ({ clientPublicKey, keyId, principals, - requestedTtl, + requestedTtl, // in ms lib format certType }: TCreateSshCertDTO) => { - // validate if the requested [certType] is allowed under the template configuration - validateSshCertificateType(template, certType); + let ttl: number | undefined; - // validate if the requested [principals] are valid for the given [certType] under the template configuration - validateSshCertificatePrincipals(certType, template, principals); + if (!template && requestedTtl) { + const parsedTtl = Math.ceil(ms(requestedTtl) / 1000); + if (parsedTtl > 0) ttl = parsedTtl; + } - // validate if the requested TTL is valid under the template configuration - const ttl = validateSshCertificateTtl(template, requestedTtl); + if (template) { + // validate if the requested [certType] is allowed under the template configuration + validateSshCertificateType(template, certType); + + // validate if the requested [principals] are valid for the given [certType] under the template configuration + validateSshCertificatePrincipals(certType, template, principals); + + // validate if the requested TTL is valid under the template configuration + ttl = validateSshCertificateTtl(template, requestedTtl); + } + + if (!ttl) { + throw new BadRequestError({ + message: "Failed to create SSH certificate due to missing TTL" + }); + } validateSshCertificateKeyId(keyId); await validateSshPublicKey(clientPublicKey); @@ -561,3 +578,23 @@ export const createSshCaHelper = async ({ return sshCertificateAuthorityDAL.transaction(processCreation); }; + +/** + * Convert an actor to a list of principals to be included in an SSH certificate. + * + * (dangtony98): This function is only supported for user actors at the moment and returns + * only the email of the associated user. In the future, we will consider other + * actor types and attributes such as group membership slugs and/or metadata to be + * included in the list of principals. + */ +export const convertActorToPrincipals = async ({ userDAL, actor, actorId }: TConvertActorToPrincipalsDTO) => { + if (actor !== ActorType.USER) { + throw new BadRequestError({ + message: "Failed to convert actor to principals due to unsupported actor type" + }); + } + + const user = await userDAL.findById(actorId); + + return [user.username]; +}; diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts index 56c6b1c76..312b7966b 100644 --- a/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts @@ -7,22 +7,14 @@ import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-cer import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal"; import { TSshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal"; import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal"; -import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types"; import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { SshCertTemplateStatus } from "../ssh-certificate-template/ssh-certificate-template-types"; +import { createSshCaHelper, createSshCert, createSshKeyPair, getSshPublicKey } from "./ssh-certificate-authority-fns"; import { - createSshCaHelper, - createSshCert, - createSshKeyPair, - getSshPublicKey, - validateExternalSshCaKeyPair -} from "./ssh-certificate-authority-fns"; -import { - SshCaKeySource, SshCaStatus, TCreateSshCaDTO, TDeleteSshCaDTO, diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-types.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-types.ts index 36a29a714..d433bd5ad 100644 --- a/backend/src/ee/services/ssh/ssh-certificate-authority-types.ts +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-types.ts @@ -5,7 +5,9 @@ import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-cer import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal"; import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types"; import { TProjectPermission } from "@app/lib/types"; +import { ActorType } from "@app/services/auth/auth-type"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TUserDALFactory } from "@app/services/user/user-dal"; export enum SshCaStatus { ACTIVE = "active", @@ -84,7 +86,7 @@ export type TGetSshCaCertificateTemplatesDTO = { } & Omit; export type TCreateSshCertDTO = { - template: TSshCertificateTemplates; + template?: TSshCertificateTemplates; caPrivateKey: string; clientPublicKey: string; keyId: string; @@ -92,3 +94,9 @@ export type TCreateSshCertDTO = { requestedTtl?: string; certType: SshCertType; }; + +export type TConvertActorToPrincipalsDTO = { + actor: ActorType; + actorId: string; + userDAL: Pick; +}; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index dcb498539..9ce3e0675 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1328,7 +1328,11 @@ export const SSH_HOSTS = { loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')", allowedPrincipals: "A list of allowed principals that can log in as the login user.", loginMappings: - "A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals." + "A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals.", + userSshCaId: + "The ID of the SSH CA to use for user certificates. If not specified, the default user SSH CA will be used if it exists.", + hostSshCaId: + "The ID of the SSH CA to use for host certificates. If not specified, the default host SSH CA will be used if it exists." }, UPDATE: { sshHostId: "The ID of the SSH host to update.", diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index cb6e28afa..bdfe8cce8 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -798,9 +798,15 @@ export const registerRoutes = async ( }); const sshHostService = sshHostServiceFactory({ + userDAL, + projectDAL, + projectSshConfigDAL, + sshCertificateAuthorityDAL, + sshCertificateAuthoritySecretDAL, sshHostDAL, sshHostLoginMappingDAL, - permissionService + permissionService, + kmsService }); const certificateAuthorityService = certificateAuthorityServiceFactory({ diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index c6dac7f5f..c6cee547d 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -1068,6 +1068,7 @@ export const projectServiceFactory = ({ actor, projectId }: TListProjectSshHostsDTO) => { + console.log("listProjectSshHosts: ", actor, actorId, actorAuthMethod, actorOrgId, projectId); const { permission } = await permissionService.getProjectPermission({ actor, actorId, diff --git a/frontend/src/pages/ssh/SshHostsPage/SshHostsPage.tsx b/frontend/src/pages/ssh/SshHostsPage/SshHostsPage.tsx index 2a0862861..31cdb586e 100644 --- a/frontend/src/pages/ssh/SshHostsPage/SshHostsPage.tsx +++ b/frontend/src/pages/ssh/SshHostsPage/SshHostsPage.tsx @@ -16,7 +16,7 @@ export const SshHostsPage = () => {
diff --git a/frontend/src/pages/ssh/SshHostsPage/components/SshHostsTable.tsx b/frontend/src/pages/ssh/SshHostsPage/components/SshHostsTable.tsx index 9cbf478ae..8b68cb581 100644 --- a/frontend/src/pages/ssh/SshHostsPage/components/SshHostsTable.tsx +++ b/frontend/src/pages/ssh/SshHostsPage/components/SshHostsTable.tsx @@ -52,17 +52,8 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => { data.map((host) => { return ( - // navigate({ - // to: `/${ProjectType.SSH}/$projectId/ca/$caId` as const, - // params: { - // projectId: currentWorkspace.id, - // caId: ca.id - // } - // }) - // } > {host.hostname}