Make PR review adjustments, ssh ca public key endpoint, ssh cert template status

This commit is contained in:
Tuan Dang
2024-12-08 21:23:00 -08:00
parent ec1ce3dc06
commit 42249726d4
23 changed files with 380 additions and 96 deletions
@@ -34,6 +34,7 @@ export async function up(knex: Knex): Promise<void> {
t.timestamps(true, true, true);
t.uuid("sshCaId").notNullable();
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
t.string("status").notNullable(); // active / disabled
t.string("name").notNullable();
t.string("ttl").notNullable();
t.string("maxTTL").notNullable();
@@ -51,7 +52,7 @@ export async function up(knex: Knex): Promise<void> {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.timestamps(true, true, true);
t.uuid("sshCaId").notNullable();
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("SET NULL");
t.uuid("sshCertificateTemplateId");
t.foreign("sshCertificateTemplateId")
.references("id")
@@ -65,7 +66,7 @@ export async function up(knex: Knex): Promise<void> {
t.datetime("notBefore").notNullable();
t.datetime("notAfter").notNullable();
});
await createOnUpdateTrigger(knex, TableName.SshCertificateTemplate);
await createOnUpdateTrigger(knex, TableName.SshCertificate);
}
}
@@ -12,6 +12,7 @@ export const SshCertificateTemplatesSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
sshCaId: z.string().uuid(),
status: z.string(),
name: z.string(),
ttl: z.string(),
maxTTL: z.string(),
@@ -109,6 +109,30 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/:sshCaId/public-key",
config: {
rateLimit: readLimit
},
schema: {
description: "Get public key of SSH CA",
params: z.object({
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET_PUBLIC_KEY.sshCaId)
}),
response: {
200: z.string()
}
},
handler: async (req) => {
const publicKey = await server.services.sshCertificateAuthority.getSshCaPublicKey({
caId: req.params.sshCaId
});
return publicKey;
}
});
server.route({
method: "PATCH",
url: "/:sshCaId",
@@ -123,10 +147,7 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => {
}),
body: z.object({
friendlyName: z.string().optional().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.friendlyName),
status: z
.enum([SshCaStatus.ACTIVE, SshCaStatus.DISABLED])
.optional()
.describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.status)
status: z.nativeEnum(SshCaStatus).optional().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.status)
}),
response: {
200: z.object({
@@ -4,6 +4,7 @@ import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
import {
isValidHostPattern,
isValidUserPattern
@@ -136,6 +137,7 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro
},
schema: {
body: z.object({
status: z.nativeEnum(SshCertTemplateStatus).optional(),
name: z
.string()
.min(1)
@@ -191,6 +193,7 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro
event: {
type: EventType.UPDATE_SSH_CERTIFICATE_TEMPLATE,
metadata: {
status: certificateTemplate.status as SshCertTemplateStatus,
certificateTemplateId: certificateTemplate.id,
sshCaId: certificateTemplate.sshCaId,
name: certificateTemplate.name,
@@ -3,6 +3,7 @@ import {
TUpdateProjectTemplateDTO
} from "@app/ee/services/project-template/project-template-types";
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
@@ -1238,6 +1239,7 @@ interface UpdateSshCertificateTemplate {
certificateTemplateId: string;
sshCaId: string;
name: string;
status: SshCertTemplateStatus;
ttl: string;
maxTTL: string;
allowedUsers: string[];
@@ -3,6 +3,7 @@ import { SshCertificateTemplatesSchema } from "@app/db/schemas";
export const sanitizedSshCertificateTemplate = SshCertificateTemplatesSchema.pick({
id: true,
sshCaId: true,
status: true,
name: true,
ttl: true,
maxTTL: true,
@@ -8,6 +8,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TSshCertificateAuthorityDALFactory } from "../ssh/ssh-certificate-authority-dal";
import { TSshCertificateTemplateDALFactory } from "./ssh-certificate-template-dal";
import {
SshCertTemplateStatus,
TCreateSshCertTemplateDTO,
TDeleteSshCertTemplateDTO,
TGetSshCertTemplateDTO,
@@ -15,7 +16,10 @@ import {
} from "./ssh-certificate-template-types";
type TSshCertificateTemplateServiceFactoryDep = {
sshCertificateTemplateDAL: TSshCertificateTemplateDALFactory;
sshCertificateTemplateDAL: Pick<
TSshCertificateTemplateDALFactory,
"transaction" | "getByName" | "create" | "updateById" | "deleteById" | "getById"
>;
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
};
@@ -62,36 +66,45 @@ export const sshCertificateTemplateServiceFactory = ({
OrgPermissionSubjects.SshCertificateTemplates
);
const existingTemplate = await sshCertificateTemplateDAL.getByName(name, ca.orgId);
if (existingTemplate) {
throw new BadRequestError({
message: `SSH certificate template with name ${name} already exists`
});
}
if (ms(ttl) > ms(maxTTL)) {
throw new BadRequestError({
message: "TTL cannot be greater than max TTL"
});
}
const certificateTemplate = await sshCertificateTemplateDAL.create({
sshCaId,
name,
ttl,
maxTTL,
allowUserCertificates,
allowHostCertificates,
allowedUsers,
allowedHosts,
allowCustomKeyIds
const newCertificateTemplate = await sshCertificateTemplateDAL.transaction(async (tx) => {
const existingTemplate = await sshCertificateTemplateDAL.getByName(name, ca.orgId, tx);
if (existingTemplate) {
throw new BadRequestError({
message: `SSH certificate template with name ${name} already exists`
});
}
const certificateTemplate = await sshCertificateTemplateDAL.create(
{
sshCaId,
name,
ttl,
maxTTL,
allowUserCertificates,
allowHostCertificates,
allowedUsers,
allowedHosts,
allowCustomKeyIds,
status: SshCertTemplateStatus.ACTIVE
},
tx
);
return certificateTemplate;
});
return { certificateTemplate, ca };
return { certificateTemplate: newCertificateTemplate, ca };
};
const updateSshCertTemplate = async ({
id,
status,
name,
ttl,
maxTTL,
@@ -125,34 +138,43 @@ export const sshCertificateTemplateServiceFactory = ({
OrgPermissionSubjects.SshCertificateTemplates
);
if (name) {
const existingTemplate = await sshCertificateTemplateDAL.getByName(name, actorOrgId);
if (existingTemplate && existingTemplate.id !== id) {
const updatedCertificateTemplate = await sshCertificateTemplateDAL.transaction(async (tx) => {
if (name) {
const existingTemplate = await sshCertificateTemplateDAL.getByName(name, actorOrgId, tx);
if (existingTemplate && existingTemplate.id !== id) {
throw new BadRequestError({
message: `SSH certificate template with name ${name} already exists`
});
}
}
if (ms(ttl || certTemplate.ttl) > ms(maxTTL || certTemplate.maxTTL)) {
throw new BadRequestError({
message: `SSH certificate template with name ${name} already exists`
message: "TTL cannot be greater than max TTL"
});
}
}
if (ms(ttl || certTemplate.ttl) > ms(maxTTL || certTemplate.maxTTL)) {
throw new BadRequestError({
message: "TTL cannot be greater than max TTL"
});
}
const certificateTemplate = await sshCertificateTemplateDAL.updateById(
id,
{
status,
name,
ttl,
maxTTL,
allowUserCertificates,
allowHostCertificates,
allowedUsers,
allowedHosts,
allowCustomKeyIds
},
tx
);
const certificateTemplate = await sshCertificateTemplateDAL.updateById(id, {
name,
ttl,
maxTTL,
allowUserCertificates,
allowHostCertificates,
allowedUsers,
allowedHosts,
allowCustomKeyIds
return certificateTemplate;
});
return {
certificateTemplate,
certificateTemplate: updatedCertificateTemplate,
orgId: certTemplate.orgId
};
};
@@ -1,5 +1,10 @@
import { TProjectPermission } from "@app/lib/types";
export enum SshCertTemplateStatus {
ACTIVE = "active",
DISABLED = "disabled"
}
export type TCreateSshCertTemplateDTO = {
sshCaId: string;
name: string;
@@ -14,6 +19,7 @@ export type TCreateSshCertTemplateDTO = {
export type TUpdateSshCertTemplateDTO = {
id: string;
status?: SshCertTemplateStatus;
name?: string;
ttl?: string;
maxTTL?: string;
@@ -8,5 +8,7 @@ export const sanitizedSshCertificate = SshCertificatesSchema.pick({
certType: true,
publicKey: true,
principals: true,
keyId: true
keyId: true,
notBefore: true,
notAfter: true
});
@@ -53,7 +53,9 @@ export const createSshKeyPair = (keyAlgorithm: CertKeyAlgorithm, comment: string
keyBits = "384";
break;
default:
throw new Error("Failed to produce SSH CA key pair generation command due to unrecognized key algorithm");
throw new BadRequestError({
message: "Failed to produce SSH CA key pair generation command due to unrecognized key algorithm"
});
}
execSync(`ssh-keygen -t ${keyType} -b ${keyBits} -f ${privateKeyFile} -N '' -C "${comment}"`);
@@ -200,7 +202,7 @@ export const validateSshCertificatePrincipals = (
* @param ttl - The TTL to validate
* @returns The TTL (in seconds) to use for issuing the SSH certificate
*/
export const validateSshCertificateTtl = (template: TSshCertificateTemplates, ttl: string | undefined) => {
export const validateSshCertificateTtl = (template: TSshCertificateTemplates, ttl?: string) => {
if (!ttl) {
// use default template ttl
return ms(template.ttl) / 1000;
@@ -249,6 +251,8 @@ export const createSshCert = ({ caPrivateKey, userPublicKey, keyId, principals,
const command = `ssh-keygen ${certOptions}`;
console.log("executing command", command);
// Execute the signing process
execSync(command);
@@ -9,6 +9,7 @@ import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certific
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { SshCertTemplateStatus } from "../ssh-certificate-template/ssh-certificate-template-types";
import {
createSshCert,
createSshKeyPair,
@@ -23,6 +24,7 @@ import {
TDeleteSshCaDTO,
TGetSshCaCertificateTemplatesDTO,
TGetSshCaDTO,
TGetSshCaPublicKeyDTO,
TIssueSshCredsDTO,
TSignSshKeyDTO,
TUpdateSshCaDTO
@@ -147,6 +149,30 @@ export const sshCertificateAuthorityServiceFactory = ({
return { ...ca, publicKey };
};
/**
* Return public key of SSH CA with id [caId]
*/
const getSshCaPublicKey = async ({ caId }: TGetSshCaPublicKeyDTO) => {
const ca = await sshCertificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: ca.id });
// decrypt secret
const orgKmsKeyId = await kmsService.getOrgKmsKeyId(ca.orgId);
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: orgKmsKeyId
});
const decryptedCaPrivateKey = await kmsDecryptor({
cipherTextBlob: sshCaSecret.encryptedPrivateKey
});
const publicKey = getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
return publicKey;
};
/**
* Update SSH CA with id [caId]
* Note: Used to enable/disable CA
@@ -259,6 +285,12 @@ export const sshCertificateAuthorityServiceFactory = ({
});
}
if (sshCertificateTemplate.status === SshCertTemplateStatus.DISABLED) {
throw new BadRequestError({
message: "SSH certificate template is disabled"
});
}
// validate if the requested [certType] is allowed under the template configuration
validateSshCertificateType(sshCertificateTemplate, certType);
@@ -359,6 +391,12 @@ export const sshCertificateAuthorityServiceFactory = ({
});
}
if (sshCertificateTemplate.status === SshCertTemplateStatus.DISABLED) {
throw new BadRequestError({
message: "SSH certificate template is disabled"
});
}
// validate if the requested [certType] is allowed under the template configuration
validateSshCertificateType(sshCertificateTemplate, certType);
@@ -445,6 +483,7 @@ export const sshCertificateAuthorityServiceFactory = ({
signSshKey,
createSshCa,
getSshCaById,
getSshCaPublicKey,
updateSshCaById,
deleteSshCaById,
getSshCaCertificateTemplates
@@ -20,6 +20,10 @@ export type TGetSshCaDTO = {
caId: string;
} & Omit<TOrgPermission, "orgId">;
export type TGetSshCaPublicKeyDTO = {
caId: string;
};
export type TUpdateSshCaDTO = {
caId: string;
friendlyName?: string;
+3
View File
@@ -1154,6 +1154,9 @@ export const SSH_CERTIFICATE_AUTHORITIES = {
GET: {
sshCaId: "The ID of the SSH CA to get."
},
GET_PUBLIC_KEY: {
sshCaId: "The ID of the SSH CA to get the public key for."
},
UPDATE: {
sshCaId: "The ID of the SSH CA to update.",
friendlyName: "A friendly name for the SSH CA to update to.",
@@ -425,7 +425,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
response: {
200: z.object({
certificates: z.array(sanitizedSshCertificate),
totalCount: z.number() // TODO
totalCount: z.number()
})
}
},