diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index bd9e483ae..6e4f196cc 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -35,6 +35,7 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service"; import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; +import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; @@ -154,6 +155,7 @@ declare module "fastify" { auditLog: TAuditLogServiceFactory; auditLogStream: TAuditLogStreamServiceFactory; certificate: TCertificateServiceFactory; + certificateTemplate: TCertificateTemplateServiceFactory; certificateAuthority: TCertificateAuthorityServiceFactory; certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory; pkiCollection: TPkiCollectionServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 0dc781092..008fee382 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -318,6 +318,11 @@ import { TWebhooksInsert, TWebhooksUpdate } from "@app/db/schemas"; +import { + TCertificateTemplates, + TCertificateTemplatesInsert, + TCertificateTemplatesUpdate +} from "@app/db/schemas/certificate-templates"; import { TSecretV2TagJunction, TSecretV2TagJunctionInsert, @@ -364,6 +369,11 @@ declare module "knex/types/tables" { TCertificateAuthorityCrlUpdate >; [TableName.Certificate]: KnexOriginal.CompositeTableType; + [TableName.CertificateTemplate]: KnexOriginal.CompositeTableType< + TCertificateTemplates, + TCertificateTemplatesInsert, + TCertificateTemplatesUpdate + >; [TableName.CertificateBody]: KnexOriginal.CompositeTableType< TCertificateBodies, TCertificateBodiesInsert, diff --git a/backend/src/db/migrations/20240815045235_add-certificate-template.ts b/backend/src/db/migrations/20240815045235_add-certificate-template.ts new file mode 100644 index 000000000..f6a691f38 --- /dev/null +++ b/backend/src/db/migrations/20240815045235_add-certificate-template.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + const hasCertificateTemplateTable = await knex.schema.hasTable(TableName.CertificateTemplate); + if (!hasCertificateTemplateTable) { + await knex.schema.createTable(TableName.CertificateTemplate, (tb) => { + tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + tb.uuid("caId").notNullable(); + tb.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE"); + tb.string("name").notNullable(); + tb.string("commonName").notNullable(); + tb.string("ttl").notNullable(); + tb.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.CertificateTemplate); + } +} + +export async function down(knex: Knex): Promise { + const hasCertificateTemplateTable = await knex.schema.hasTable(TableName.CertificateTemplate); + if (hasCertificateTemplateTable) { + await knex.schema.dropTable(TableName.CertificateTemplate); + await dropOnUpdateTrigger(knex, TableName.CertificateTemplate); + } +} diff --git a/backend/src/db/schemas/certificate-templates.ts b/backend/src/db/schemas/certificate-templates.ts new file mode 100644 index 000000000..988147362 --- /dev/null +++ b/backend/src/db/schemas/certificate-templates.ts @@ -0,0 +1,22 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const CertificateTemplatesSchema = z.object({ + id: z.string().uuid(), + caId: z.string().uuid(), + name: z.string(), + commonName: z.string(), + ttl: z.string(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TCertificateTemplates = z.infer; +export type TCertificateTemplatesInsert = Omit, TImmutableDBKeys>; +export type TCertificateTemplatesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index ebbead70c..03f7d6a3b 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -9,6 +9,7 @@ export enum TableName { Certificate = "certificates", CertificateBody = "certificate_bodies", CertificateSecret = "certificate_secrets", + CertificateTemplate = "certificate_templates", PkiAlert = "pki_alerts", PkiCollection = "pki_collections", PkiCollectionItem = "pki_collection_items", diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 7ea2ba39f..f8f19a295 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -30,6 +30,7 @@ export enum ProjectPermissionSub { Identity = "identity", CertificateAuthorities = "certificate-authorities", Certificates = "certificates", + CertificateTemplates = "certificate-templates", PkiAlerts = "pki-alerts", PkiCollections = "pki-collections", Kms = "kms" @@ -65,6 +66,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.Identity] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.Certificates] + | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] @@ -165,6 +167,11 @@ const buildAdminPermissionRules = () => { can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates); + can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates); + can(ProjectPermissionActions.Create, ProjectPermissionSub.CertificateTemplates); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateTemplates); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateTemplates); + can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts); can(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts); can(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts); @@ -251,6 +258,8 @@ const buildMemberPermissionRules = () => { can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates); + can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates); + can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts); can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections); diff --git a/backend/src/server/routes/v1/certificate-template-router.ts b/backend/src/server/routes/v1/certificate-template-router.ts new file mode 100644 index 000000000..854c668ba --- /dev/null +++ b/backend/src/server/routes/v1/certificate-template-router.ts @@ -0,0 +1,116 @@ +import { z } from "zod"; + +import { CertificateTemplatesSchema } from "@app/db/schemas/certificate-templates"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({ + id: true, + caId: true, + name: true, + commonName: true, + ttl: true +}); + +export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/:certificateTemplateId", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + certificateTemplateId: z.string() + }), + response: { + 200: z.object({ + certificateTemplate: sanitizedCertificateTemplate.merge( + z.object({ + projectId: z.string(), + caName: z.string() + }) + ) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const certificateTemplate = await server.services.certificateTemplate.getCertTemplate({ + id: req.params.certificateTemplateId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + return { certificateTemplate }; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + caId: z.string(), + name: z.string(), + commonName: z.string(), + ttl: z.string() + }), + response: { + 200: z.object({ + certificateTemplate: sanitizedCertificateTemplate + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const certificateTemplate = await server.services.certificateTemplate.createCertTemplate({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + return { certificateTemplate }; + } + }); + + server.route({ + method: "PATCH", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({}), + response: { + 200: z.object({}) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => {} + }); + + server.route({ + method: "DELETE", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({}), + response: { + 200: z.object({}) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => {} + }); +}; diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 85043ecee..ed80a9e59 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -8,6 +8,7 @@ import { PkiCollectionsSchema, ProjectKeysSchema } from "@app/db/schemas"; +import { CertificateTemplatesSchema } from "@app/db/schemas/certificate-templates"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { PROJECTS } from "@app/lib/api-docs"; import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -458,4 +459,39 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { return { collections: pkiCollections }; } }); + + server.route({ + method: "GET", + url: "/:projectId/certificate-templates", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + projectId: z.string().trim() + }), + response: { + 200: z.object({ + certificateTemplates: z.array( + CertificateTemplatesSchema.pick({ + id: true, + name: true + }) + ) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { certificateTemplates } = await server.services.project.listProjectCertificateTemplates({ + projectId: req.params.projectId, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type + }); + + return { certificateTemplates }; + } + }); }; diff --git a/backend/src/services/certificate-template/certificate-template-dal.ts b/backend/src/services/certificate-template/certificate-template-dal.ts new file mode 100644 index 000000000..062e64f35 --- /dev/null +++ b/backend/src/services/certificate-template/certificate-template-dal.ts @@ -0,0 +1,44 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; + +export type TCertificateTemplateDALFactory = ReturnType; + +export const certificateTemplateDALFactory = (db: TDbClient) => { + const certificateTemplateOrm = ormify(db, TableName.CertificateTemplate); + + const getCertTemplatesByProjectId = async (projectId: string) => { + const certTemplates = await db + .replicaNode()(TableName.CertificateTemplate) + .join( + TableName.CertificateAuthority, + `${TableName.CertificateAuthority}.id`, + `${TableName.CertificateTemplate}.caId` + ) + .where(`${TableName.CertificateAuthority}.projectId`, "=", projectId) + .select(selectAllTableCols(TableName.CertificateTemplate)); + + return certTemplates; + }; + + const getById = async (id: string) => { + const certTemplate = await db + .replicaNode()(TableName.CertificateTemplate) + .join( + TableName.CertificateAuthority, + `${TableName.CertificateAuthority}.id`, + `${TableName.CertificateTemplate}.caId` + ) + .where(`${TableName.CertificateTemplate}.id`, "=", id) + .select(selectAllTableCols(TableName.CertificateTemplate)) + .select( + db.ref("projectId").withSchema(TableName.CertificateAuthority), + db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority) + ) + .first(); + + return certTemplate; + }; + + return { ...certificateTemplateOrm, getCertTemplatesByProjectId, getById }; +}; diff --git a/backend/src/services/certificate-template/certificate-template-service.ts b/backend/src/services/certificate-template/certificate-template-service.ts new file mode 100644 index 000000000..2739c4b45 --- /dev/null +++ b/backend/src/services/certificate-template/certificate-template-service.ts @@ -0,0 +1,91 @@ +import { ForbiddenError } from "@casl/ability"; + +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { NotFoundError } from "@app/lib/errors"; + +import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; +import { TCertificateTemplateDALFactory } from "./certificate-template-dal"; +import { TCreateCertTemplateDTO, TGetCertTemplateDTO } from "./certificate-template-types"; + +type TCertificateTemplateServiceFactoryDep = { + certificateTemplateDAL: TCertificateTemplateDALFactory; + certificateAuthorityDAL: Pick; + permissionService: Pick; +}; + +export type TCertificateTemplateServiceFactory = ReturnType; + +export const certificateTemplateServiceFactory = ({ + certificateTemplateDAL, + certificateAuthorityDAL, + permissionService +}: TCertificateTemplateServiceFactoryDep) => { + const createCertTemplate = async ({ + caId, + name, + commonName, + ttl, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TCreateCertTemplateDTO) => { + const ca = await certificateAuthorityDAL.findById(caId); + if (!ca) { + throw new NotFoundError({ + message: "CA not found" + }); + } + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + ca.projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.CertificateTemplates + ); + + const certificateTemplate = await certificateTemplateDAL.create({ + caId, + name, + commonName, + ttl + }); + + return certificateTemplate; + }; + + const getCertTemplate = async ({ id, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertTemplateDTO) => { + const certTemplate = await certificateTemplateDAL.getById(id); + if (!certTemplate) { + throw new NotFoundError({ + message: "Certificate template not found." + }); + } + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + certTemplate.projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.CertificateTemplates + ); + + return certTemplate; + }; + + return { + createCertTemplate, + getCertTemplate + }; +}; diff --git a/backend/src/services/certificate-template/certificate-template-types.ts b/backend/src/services/certificate-template/certificate-template-types.ts new file mode 100644 index 000000000..471666e19 --- /dev/null +++ b/backend/src/services/certificate-template/certificate-template-types.ts @@ -0,0 +1,12 @@ +import { TProjectPermission } from "@app/lib/types"; + +export type TCreateCertTemplateDTO = { + caId: string; + name: string; + commonName: string; + ttl: string; +} & Omit; + +export type TGetCertTemplateDTO = { + id: string; +} & Omit; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 9416a66a9..a7111d784 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -16,6 +16,7 @@ import { TProjectPermission } from "@app/lib/types"; import { ActorType } from "../auth/auth-type"; import { TCertificateDALFactory } from "../certificate/certificate-dal"; import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; +import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; @@ -41,6 +42,7 @@ import { TGetProjectKmsKey, TListProjectAlertsDTO, TListProjectCasDTO, + TListProjectCertificateTemplatesDTO, TListProjectCertsDTO, TLoadProjectKmsBackupDTO, TToggleProjectAutoCapitalizationDTO, @@ -73,6 +75,7 @@ type TProjectServiceFactoryDep = { projectUserMembershipRoleDAL: Pick; certificateAuthorityDAL: Pick; certificateDAL: Pick; + certificateTemplateDAL: Pick; pkiAlertDAL: Pick; pkiCollectionDAL: Pick; permissionService: TPermissionServiceFactory; @@ -112,6 +115,7 @@ export const projectServiceFactory = ({ identityProjectMembershipRoleDAL, certificateAuthorityDAL, certificateDAL, + certificateTemplateDAL, pkiCollectionDAL, pkiAlertDAL, keyStore, @@ -737,6 +741,36 @@ export const projectServiceFactory = ({ }; }; + /** + * Return list of certificate templates for project + */ + const listProjectCertificateTemplates = async ({ + projectId, + actorId, + actorOrgId, + actorAuthMethod, + actor + }: TListProjectCertificateTemplatesDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.CertificateTemplates + ); + + const certificateTemplates = await certificateTemplateDAL.getCertTemplatesByProjectId(projectId); + + return { + certificateTemplates + }; + }; + const updateProjectKmsKey = async ({ projectId, kms, @@ -857,6 +891,7 @@ export const projectServiceFactory = ({ listProjectCertificates, listProjectAlerts, listProjectPkiCollections, + listProjectCertificateTemplates, updateVersionLimit, updateAuditLogsRetention, updateProjectKmsKey, diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index ac53b3f71..ee2f1aea9 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -117,3 +117,5 @@ export type TLoadProjectKmsBackupDTO = { } & TProjectPermission; export type TGetProjectKmsKey = TProjectPermission; + +export type TListProjectCertificateTemplatesDTO = TProjectPermission;