Add SSH certificate tab + data structure

This commit is contained in:
Tuan Dang
2024-12-05 23:01:28 -08:00
parent 5b618b07fa
commit 6e720c2f64
33 changed files with 604 additions and 210 deletions
@@ -7,15 +7,6 @@ export enum OrgPermissionActions {
Delete = "delete"
}
export enum OrgPermissionSshCertificateTemplateActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
SignSshKey = "sign-ssh-key",
IssueSshCredentials = "issue-ssh-credentials"
}
export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects"
}
@@ -37,6 +28,7 @@ export enum OrgPermissionSubjects {
AdminConsole = "organization-admin-console",
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates",
SshCertificates = "ssh-certificates",
SshCertificateAuthorities = "ssh-certificate-authorities",
SshCertificateTemplates = "ssh-certificate-templates"
}
@@ -59,7 +51,8 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionActions, OrgPermissionSubjects.SshCertificateAuthorities]
| [OrgPermissionSshCertificateTemplateActions, OrgPermissionSubjects.SshCertificateTemplates];
| [OrgPermissionActions, OrgPermissionSubjects.SshCertificates]
| [OrgPermissionActions, OrgPermissionSubjects.SshCertificateTemplates];
const buildAdminPermission = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
@@ -136,22 +129,18 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificates);
can(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificates);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateAuthorities);
can(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificateAuthorities);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SshCertificateAuthorities);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SshCertificateAuthorities);
can(
[
OrgPermissionSshCertificateTemplateActions.Read,
OrgPermissionSshCertificateTemplateActions.Create,
OrgPermissionSshCertificateTemplateActions.Edit,
OrgPermissionSshCertificateTemplateActions.Delete,
OrgPermissionSshCertificateTemplateActions.SignSshKey,
OrgPermissionSshCertificateTemplateActions.IssueSshCredentials
],
OrgPermissionSubjects.SshCertificateTemplates
);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateTemplates);
can(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificateTemplates);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SshCertificateTemplates);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SshCertificateTemplates);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
@@ -184,9 +173,9 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateAuthorities);
can(OrgPermissionSshCertificateTemplateActions.Read, OrgPermissionSubjects.SshCertificateTemplates);
can(OrgPermissionSshCertificateTemplateActions.SignSshKey, OrgPermissionSubjects.SshCertificateTemplates);
can(OrgPermissionSshCertificateTemplateActions.IssueSshCredentials, OrgPermissionSubjects.SshCertificateTemplates);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificates);
can(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificates);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SshCertificateTemplates);
return rules;
};
@@ -1,10 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import ms from "ms";
import {
OrgPermissionSshCertificateTemplateActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -61,7 +58,7 @@ export const sshCertificateTemplateServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionSshCertificateTemplateActions.Create,
OrgPermissionActions.Create,
OrgPermissionSubjects.SshCertificateTemplates
);
@@ -124,7 +121,7 @@ export const sshCertificateTemplateServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionSshCertificateTemplateActions.Edit,
OrgPermissionActions.Edit,
OrgPermissionSubjects.SshCertificateTemplates
);
@@ -183,7 +180,7 @@ export const sshCertificateTemplateServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionSshCertificateTemplateActions.Delete,
OrgPermissionActions.Delete,
OrgPermissionSubjects.SshCertificateTemplates
);
@@ -209,7 +206,7 @@ export const sshCertificateTemplateServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionSshCertificateTemplateActions.Read,
OrgPermissionActions.Read,
OrgPermissionSubjects.SshCertificateTemplates
);
@@ -0,0 +1,38 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TSshCertificateDALFactory = ReturnType<typeof sshCertificateDALFactory>;
export const sshCertificateDALFactory = (db: TDbClient) => {
const sshCertificateOrm = ormify(db, TableName.SshCertificate);
const countSshCertificatesInOrg = async (orgId: string) => {
try {
interface CountResult {
count: string;
}
const query = db
.replicaNode()(TableName.SshCertificate)
.join(
TableName.SshCertificateAuthority,
`${TableName.SshCertificate}.sshCaId`,
`${TableName.SshCertificateAuthority}.id`
)
.join(TableName.Organization, `${TableName.SshCertificateAuthority}.orgId`, `${TableName.Organization}.id`)
.where(`${TableName.Organization}.id`, orgId);
const count = await query.count("*").first();
return parseInt((count as unknown as CountResult).count || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "Count all SSH certificates in organization" });
}
};
return {
...sshCertificateOrm,
countSshCertificatesInOrg
};
};
@@ -0,0 +1,12 @@
import { SshCertificatesSchema } from "@app/db/schemas";
export const sanitizedSshCertificate = SshCertificatesSchema.pick({
id: true,
sshCaId: true,
sshCertificateTemplateId: true,
serialNumber: true,
certType: true,
publicKey: true,
principals: true,
keyId: true
});
@@ -1,15 +1,12 @@
import { ForbiddenError } from "@casl/ability";
import {
OrgPermissionActions,
OrgPermissionSshCertificateTemplateActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
import { NotFoundError } from "@app/lib/errors";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import {
@@ -38,6 +35,7 @@ type TSshCertificateAuthorityServiceFactoryDep = {
>;
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create" | "findOne">;
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find" | "getByName">;
sshCertificateDAL: Pick<TSshCertificateDALFactory, "create">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey" | "getOrgKmsKeyId">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
};
@@ -48,6 +46,7 @@ export const sshCertificateAuthorityServiceFactory = ({
sshCertificateAuthorityDAL,
sshCertificateAuthoritySecretDAL,
sshCertificateTemplateDAL,
sshCertificateDAL,
kmsService,
permissionService
}: TSshCertificateAuthorityServiceFactoryDep) => {
@@ -252,10 +251,13 @@ export const sshCertificateAuthorityServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionSshCertificateTemplateActions.IssueSshCredentials,
OrgPermissionSubjects.SshCertificateTemplates
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificates);
if (sshCertificateTemplate.caStatus === SshCaStatus.DISABLED) {
throw new BadRequestError({
message: "SSH CA is disabled"
});
}
// validate if the requested [certType] is allowed under the template configuration
validateSshCertificateType(sshCertificateTemplate, certType);
@@ -295,6 +297,18 @@ export const sshCertificateAuthorityServiceFactory = ({
certType
});
await sshCertificateDAL.create({
sshCaId: sshCertificateTemplate.sshCaId,
sshCertificateTemplateId: sshCertificateTemplate.id,
serialNumber,
certType,
publicKey,
principals,
keyId,
notBefore: new Date(),
notAfter: new Date(Date.now() + ttl * 1000)
});
return {
serialNumber,
signedPublicKey,
@@ -337,10 +351,13 @@ export const sshCertificateAuthorityServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionSshCertificateTemplateActions.SignSshKey,
OrgPermissionSubjects.SshCertificateTemplates
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SshCertificates);
if (sshCertificateTemplate.caStatus === SshCaStatus.DISABLED) {
throw new BadRequestError({
message: "SSH CA is disabled"
});
}
// validate if the requested [certType] is allowed under the template configuration
validateSshCertificateType(sshCertificateTemplate, certType);
@@ -377,6 +394,18 @@ export const sshCertificateAuthorityServiceFactory = ({
certType
});
await sshCertificateDAL.create({
sshCaId: sshCertificateTemplate.sshCaId,
sshCertificateTemplateId: sshCertificateTemplate.id,
serialNumber,
certType,
publicKey,
principals,
keyId,
notBefore: new Date(),
notAfter: new Date(Date.now() + ttl * 1000)
});
return { serialNumber, signedPublicKey, certificateTemplate: sshCertificateTemplate, ttl, keyId };
};
@@ -399,7 +428,7 @@ export const sshCertificateAuthorityServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionSshCertificateTemplateActions.Read,
OrgPermissionActions.Read,
OrgPermissionSubjects.SshCertificateTemplates
);