Add list accessible ssh hosts endpoint

This commit is contained in:
Tuan Dang
2025-04-06 17:28:46 -07:00
parent c2f2a038ad
commit 715bb447e6
12 changed files with 429 additions and 70 deletions
@@ -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<TUserDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "find">;
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "findOne">;
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findById">;
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "findOne">;
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<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TSshHostServiceFactory = ReturnType<typeof sshHostServiceFactory>;
@@ -29,17 +59,72 @@ export type TSshHostServiceFactory = ReturnType<typeof sshHostServiceFactory>;
*/
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, [
"[email protected]" // 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
};
};