mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #2291 from Infisical/feature/certificate-template
feat: certificate templates
This commit is contained in:
24
backend/package-lock.json
generated
24
backend/package-lock.json
generated
@@ -76,6 +76,7 @@
|
||||
"pino": "^8.16.2",
|
||||
"posthog-node": "^3.6.2",
|
||||
"probot": "^13.0.0",
|
||||
"safe-regex": "^2.1.1",
|
||||
"smee-client": "^2.0.0",
|
||||
"tedious": "^18.2.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
@@ -107,6 +108,7 @@
|
||||
"@types/picomatch": "^2.3.3",
|
||||
"@types/prompt-sync": "^4.2.3",
|
||||
"@types/resolve": "^1.20.6",
|
||||
"@types/safe-regex": "^1.1.6",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
@@ -9801,6 +9803,12 @@
|
||||
"integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/safe-regex": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/safe-regex/-/safe-regex-1.1.6.tgz",
|
||||
"integrity": "sha512-CQ/uPB9fLOPKwDsrTeVbNIkwfUthTWOx0l6uIGwVFjZxv7e68pCW5gtTYFzdJi3EBJp8h8zYhJbTasAbX7gEMQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
||||
@@ -17821,6 +17829,14 @@
|
||||
"@babel/runtime": "^7.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/regexp-tree": {
|
||||
"version": "0.1.27",
|
||||
"resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
|
||||
"integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
|
||||
"bin": {
|
||||
"regexp-tree": "bin/regexp-tree"
|
||||
}
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
|
||||
@@ -18137,6 +18153,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/safe-regex": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz",
|
||||
"integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==",
|
||||
"dependencies": {
|
||||
"regexp-tree": "~0.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-regex-test": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
"@types/picomatch": "^2.3.3",
|
||||
"@types/prompt-sync": "^4.2.3",
|
||||
"@types/resolve": "^1.20.6",
|
||||
"@types/safe-regex": "^1.1.6",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
@@ -172,6 +173,7 @@
|
||||
"pino": "^8.16.2",
|
||||
"posthog-node": "^3.6.2",
|
||||
"probot": "^13.0.0",
|
||||
"safe-regex": "^2.1.1",
|
||||
"smee-client": "^2.0.0",
|
||||
"tedious": "^18.2.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
|
||||
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -36,6 +36,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";
|
||||
@@ -156,6 +157,7 @@ declare module "fastify" {
|
||||
auditLog: TAuditLogServiceFactory;
|
||||
auditLogStream: TAuditLogStreamServiceFactory;
|
||||
certificate: TCertificateServiceFactory;
|
||||
certificateTemplate: TCertificateTemplateServiceFactory;
|
||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||
pkiCollection: TPkiCollectionServiceFactory;
|
||||
|
||||
8
backend/src/@types/knex.d.ts
vendored
8
backend/src/@types/knex.d.ts
vendored
@@ -53,6 +53,9 @@ import {
|
||||
TCertificateSecretsUpdate,
|
||||
TCertificatesInsert,
|
||||
TCertificatesUpdate,
|
||||
TCertificateTemplates,
|
||||
TCertificateTemplatesInsert,
|
||||
TCertificateTemplatesUpdate,
|
||||
TDynamicSecretLeases,
|
||||
TDynamicSecretLeasesInsert,
|
||||
TDynamicSecretLeasesUpdate,
|
||||
@@ -364,6 +367,11 @@ declare module "knex/types/tables" {
|
||||
TCertificateAuthorityCrlUpdate
|
||||
>;
|
||||
[TableName.Certificate]: KnexOriginal.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>;
|
||||
[TableName.CertificateTemplate]: KnexOriginal.CompositeTableType<
|
||||
TCertificateTemplates,
|
||||
TCertificateTemplatesInsert,
|
||||
TCertificateTemplatesUpdate
|
||||
>;
|
||||
[TableName.CertificateBody]: KnexOriginal.CompositeTableType<
|
||||
TCertificateBodies,
|
||||
TCertificateBodiesInsert,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
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.uuid("pkiCollectionId");
|
||||
tb.foreign("pkiCollectionId").references("id").inTable(TableName.PkiCollection).onDelete("SET NULL");
|
||||
tb.string("name").notNullable();
|
||||
tb.string("commonName").notNullable();
|
||||
tb.string("subjectAlternativeName").notNullable();
|
||||
tb.string("ttl").notNullable();
|
||||
tb.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.CertificateTemplate);
|
||||
}
|
||||
|
||||
const doesCertificateTableHaveTemplateId = await knex.schema.hasColumn(
|
||||
TableName.Certificate,
|
||||
"certificateTemplateId"
|
||||
);
|
||||
|
||||
if (!doesCertificateTableHaveTemplateId) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (tb) => {
|
||||
tb.uuid("certificateTemplateId");
|
||||
tb.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplate).onDelete("SET NULL");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const doesCertificateTableHaveTemplateId = await knex.schema.hasColumn(
|
||||
TableName.Certificate,
|
||||
"certificateTemplateId"
|
||||
);
|
||||
|
||||
if (doesCertificateTableHaveTemplateId) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.dropColumn("certificateTemplateId");
|
||||
});
|
||||
}
|
||||
|
||||
const hasCertificateTemplateTable = await knex.schema.hasTable(TableName.CertificateTemplate);
|
||||
if (hasCertificateTemplateTable) {
|
||||
await knex.schema.dropTable(TableName.CertificateTemplate);
|
||||
await dropOnUpdateTrigger(knex, TableName.CertificateTemplate);
|
||||
}
|
||||
}
|
||||
24
backend/src/db/schemas/certificate-templates.ts
Normal file
24
backend/src/db/schemas/certificate-templates.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// 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(),
|
||||
pkiCollectionId: z.string().uuid().nullable().optional(),
|
||||
name: z.string(),
|
||||
commonName: z.string(),
|
||||
subjectAlternativeName: z.string(),
|
||||
ttl: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TCertificateTemplates = z.infer<typeof CertificateTemplatesSchema>;
|
||||
export type TCertificateTemplatesInsert = Omit<z.input<typeof CertificateTemplatesSchema>, TImmutableDBKeys>;
|
||||
export type TCertificateTemplatesUpdate = Partial<Omit<z.input<typeof CertificateTemplatesSchema>, TImmutableDBKeys>>;
|
||||
@@ -21,7 +21,8 @@ export const CertificatesSchema = z.object({
|
||||
revokedAt: z.date().nullable().optional(),
|
||||
revocationReason: z.number().nullable().optional(),
|
||||
altNames: z.string().default("").nullable().optional(),
|
||||
caCertId: z.string().uuid()
|
||||
caCertId: z.string().uuid(),
|
||||
certificateTemplateId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
||||
|
||||
@@ -14,6 +14,7 @@ export * from "./certificate-authority-crl";
|
||||
export * from "./certificate-authority-secret";
|
||||
export * from "./certificate-bodies";
|
||||
export * from "./certificate-secrets";
|
||||
export * from "./certificate-templates";
|
||||
export * from "./certificates";
|
||||
export * from "./dynamic-secret-leases";
|
||||
export * from "./dynamic-secrets";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -162,7 +162,11 @@ export enum EventType {
|
||||
UPDATE_PROJECT_KMS = "update-project-kms",
|
||||
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
|
||||
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
|
||||
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project"
|
||||
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
|
||||
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
||||
GET_CERTIFICATE_TEMPLATE = "get-certificate-template"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
@@ -1366,6 +1370,46 @@ interface LoadProjectKmsBackupEvent {
|
||||
metadata: Record<string, string>; // no metadata yet
|
||||
}
|
||||
|
||||
interface CreateCertificateTemplate {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
caId: string;
|
||||
pkiCollectionId?: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCertificateTemplate {
|
||||
type: EventType.GET_CERTIFICATE_TEMPLATE;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateCertificateTemplate {
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
caId: string;
|
||||
pkiCollectionId?: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteCertificateTemplate {
|
||||
type: EventType.DELETE_CERTIFICATE_TEMPLATE;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface OrgAdminAccessProjectEvent {
|
||||
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
|
||||
metadata: {
|
||||
@@ -1499,4 +1543,8 @@ export type Event =
|
||||
| UpdateProjectKmsEvent
|
||||
| GetProjectKmsBackupEvent
|
||||
| LoadProjectKmsBackupEvent
|
||||
| OrgAdminAccessProjectEvent;
|
||||
| OrgAdminAccessProjectEvent
|
||||
| CreateCertificateTemplate
|
||||
| UpdateCertificateTemplate
|
||||
| GetCertificateTemplate
|
||||
| DeleteCertificateTemplate;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1089,6 +1089,7 @@ export const CERTIFICATE_AUTHORITIES = {
|
||||
},
|
||||
ISSUE_CERT: {
|
||||
caId: "The ID of the CA to issue the certificate from",
|
||||
certificateTemplateId: "The ID of the certificate template to issue the certificate from",
|
||||
pkiCollectionId: "The ID of the PKI collection to add the certificate to",
|
||||
friendlyName: "A friendly name for the certificate",
|
||||
commonName: "The common name (CN) for the certificate",
|
||||
@@ -1147,6 +1148,32 @@ export const CERTIFICATES = {
|
||||
}
|
||||
};
|
||||
|
||||
export const CERTIFICATE_TEMPLATES = {
|
||||
CREATE: {
|
||||
caId: "The ID of the certificate authority to associate the template with",
|
||||
pkiCollectionId: "The ID of the PKI collection to bind to the template",
|
||||
name: "The name of the template",
|
||||
commonName: "The regular expression string to use for validating common names",
|
||||
subjectAlternativeName: "The regular expression string to use for validating subject alternative names",
|
||||
ttl: "The max TTL for the template"
|
||||
},
|
||||
GET: {
|
||||
certificateTemplateId: "The ID of the certificate template to get"
|
||||
},
|
||||
UPDATE: {
|
||||
certificateTemplateId: "The ID of the certificate template to update",
|
||||
caId: "The ID of the certificate authority to update the association with the template",
|
||||
pkiCollectionId: "The ID of the PKI collection to update the binding to the template",
|
||||
name: "The updated name of the template",
|
||||
commonName: "The updated regular expression string for validating common names",
|
||||
subjectAlternativeName: "The updated regular expression string for validating subject alternative names",
|
||||
ttl: "The updated max TTL for the template"
|
||||
},
|
||||
DELETE: {
|
||||
certificateTemplateId: "The ID of the certificate template to delete"
|
||||
}
|
||||
};
|
||||
|
||||
export const ALERTS = {
|
||||
CREATE: {
|
||||
projectId: "The ID of the project to create the alert in",
|
||||
|
||||
@@ -89,6 +89,8 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author
|
||||
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
|
||||
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
||||
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
||||
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||
@@ -587,6 +589,7 @@ export const registerRoutes = async (
|
||||
const certificateAuthorityCertDAL = certificateAuthorityCertDALFactory(db);
|
||||
const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
|
||||
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
|
||||
const certificateTemplateDAL = certificateTemplateDALFactory(db);
|
||||
|
||||
const certificateDAL = certificateDALFactory(db);
|
||||
const certificateBodyDAL = certificateBodyDALFactory(db);
|
||||
@@ -622,6 +625,7 @@ export const registerRoutes = async (
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityQueue,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
@@ -641,6 +645,12 @@ export const registerRoutes = async (
|
||||
licenseService
|
||||
});
|
||||
|
||||
const certificateTemplateService = certificateTemplateServiceFactory({
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityDAL,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const pkiAlertService = pkiAlertServiceFactory({
|
||||
pkiAlertDAL,
|
||||
pkiCollectionDAL,
|
||||
@@ -678,7 +688,8 @@ export const registerRoutes = async (
|
||||
identityProjectMembershipRoleDAL,
|
||||
keyStore,
|
||||
kmsService,
|
||||
projectBotDAL
|
||||
projectBotDAL,
|
||||
certificateTemplateDAL
|
||||
});
|
||||
|
||||
const projectEnvService = projectEnvServiceFactory({
|
||||
@@ -1160,6 +1171,7 @@ export const registerRoutes = async (
|
||||
auditLogStream: auditLogStreamService,
|
||||
certificate: certificateService,
|
||||
certificateAuthority: certificateAuthorityService,
|
||||
certificateTemplate: certificateTemplateService,
|
||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||
pkiAlert: pkiAlertService,
|
||||
pkiCollection: pkiCollectionService,
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificatesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { CERTIFICATES } from "@app/lib/api-docs";
|
||||
import { CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
|
||||
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";
|
||||
import { CrlReason } from "@app/services/certificate/certificate-types";
|
||||
import {
|
||||
validateAltNamesField,
|
||||
validateCaDateField
|
||||
} from "@app/services/certificate-authority/certificate-authority-validators";
|
||||
|
||||
export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@@ -55,6 +60,185 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/issue-certificate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Issue certificate",
|
||||
body: z
|
||||
.object({
|
||||
caId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.caId),
|
||||
certificateTemplateId: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateTemplateId),
|
||||
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId),
|
||||
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
|
||||
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
|
||||
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
|
||||
ttl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
|
||||
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore),
|
||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter)
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const { ttl, notAfter } = data;
|
||||
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
|
||||
},
|
||||
{
|
||||
message: "Either ttl or notAfter must be present, but not both",
|
||||
path: ["ttl", "notAfter"]
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) =>
|
||||
(data.caId !== undefined && data.certificateTemplateId === undefined) ||
|
||||
(data.caId === undefined && data.certificateTemplateId !== undefined),
|
||||
{
|
||||
message: "Either CA ID or Certificate Template ID must be present, but not both",
|
||||
path: ["caId", "certificateTemplateId"]
|
||||
}
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificate),
|
||||
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
|
||||
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
|
||||
privateKey: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.privateKey),
|
||||
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } =
|
||||
await server.services.certificateAuthority.issueCertFromCa({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
event: {
|
||||
type: EventType.ISSUE_CERT,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn,
|
||||
serialNumber
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
privateKey,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/sign-certificate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Sign certificate",
|
||||
body: z
|
||||
.object({
|
||||
caId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId),
|
||||
certificateTemplateId: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateTemplateId),
|
||||
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId),
|
||||
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr),
|
||||
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName),
|
||||
commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName),
|
||||
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames),
|
||||
ttl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
|
||||
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
|
||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter)
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const { ttl, notAfter } = data;
|
||||
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
|
||||
},
|
||||
{
|
||||
message: "Either ttl or notAfter must be present, but not both",
|
||||
path: ["ttl", "notAfter"]
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) =>
|
||||
(data.caId !== undefined && data.certificateTemplateId === undefined) ||
|
||||
(data.caId === undefined && data.certificateTemplateId !== undefined),
|
||||
{
|
||||
message: "Either CA ID or Certificate Template ID must be present, but not both",
|
||||
path: ["caId", "certificateTemplateId"]
|
||||
}
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.certificate),
|
||||
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
|
||||
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
|
||||
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||
await server.services.certificateAuthority.signCertFromCa({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
event: {
|
||||
type: EventType.SIGN_CERT,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn,
|
||||
serialNumber
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:serialNumber/revoke",
|
||||
|
||||
205
backend/src/server/routes/v1/certificate-template-router.ts
Normal file
205
backend/src/server/routes/v1/certificate-template-router.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
|
||||
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";
|
||||
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
|
||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||
|
||||
export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:certificateTemplateId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.GET.certificateTemplateId)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedCertificateTemplate
|
||||
}
|
||||
},
|
||||
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
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: certificateTemplate.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CERTIFICATE_TEMPLATE,
|
||||
metadata: {
|
||||
certificateTemplateId: certificateTemplate.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return certificateTemplate;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
caId: z.string().describe(CERTIFICATE_TEMPLATES.CREATE.caId),
|
||||
pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.CREATE.pkiCollectionId),
|
||||
name: z.string().min(1).describe(CERTIFICATE_TEMPLATES.CREATE.name),
|
||||
commonName: validateTemplateRegexField.describe(CERTIFICATE_TEMPLATES.CREATE.commonName),
|
||||
subjectAlternativeName: validateTemplateRegexField.describe(
|
||||
CERTIFICATE_TEMPLATES.CREATE.subjectAlternativeName
|
||||
),
|
||||
ttl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(CERTIFICATE_TEMPLATES.CREATE.ttl)
|
||||
}),
|
||||
response: {
|
||||
200: 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
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: certificateTemplate.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE,
|
||||
metadata: {
|
||||
certificateTemplateId: certificateTemplate.id,
|
||||
caId: certificateTemplate.caId,
|
||||
pkiCollectionId: certificateTemplate.pkiCollectionId as string,
|
||||
name: certificateTemplate.name,
|
||||
commonName: certificateTemplate.commonName,
|
||||
subjectAlternativeName: certificateTemplate.subjectAlternativeName,
|
||||
ttl: certificateTemplate.ttl
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return certificateTemplate;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:certificateTemplateId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
caId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.caId),
|
||||
pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.pkiCollectionId),
|
||||
name: z.string().min(1).optional().describe(CERTIFICATE_TEMPLATES.UPDATE.name),
|
||||
commonName: validateTemplateRegexField.optional().describe(CERTIFICATE_TEMPLATES.UPDATE.commonName),
|
||||
subjectAlternativeName: validateTemplateRegexField
|
||||
.optional()
|
||||
.describe(CERTIFICATE_TEMPLATES.UPDATE.subjectAlternativeName),
|
||||
ttl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.optional()
|
||||
.describe(CERTIFICATE_TEMPLATES.UPDATE.ttl)
|
||||
}),
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedCertificateTemplate
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const certificateTemplate = await server.services.certificateTemplate.updateCertTemplate({
|
||||
...req.body,
|
||||
id: req.params.certificateTemplateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: certificateTemplate.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE,
|
||||
metadata: {
|
||||
certificateTemplateId: certificateTemplate.id,
|
||||
caId: certificateTemplate.caId,
|
||||
pkiCollectionId: certificateTemplate.pkiCollectionId as string,
|
||||
name: certificateTemplate.name,
|
||||
commonName: certificateTemplate.commonName,
|
||||
subjectAlternativeName: certificateTemplate.subjectAlternativeName,
|
||||
ttl: certificateTemplate.ttl
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return certificateTemplate;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:certificateTemplateId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.DELETE.certificateTemplateId)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedCertificateTemplate
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const certificateTemplate = await server.services.certificateTemplate.deleteCertTemplate({
|
||||
id: req.params.certificateTemplateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: certificateTemplate.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_CERTIFICATE_TEMPLATE,
|
||||
metadata: {
|
||||
certificateTemplateId: certificateTemplate.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return certificateTemplate;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { registerAuthRoutes } from "./auth-router";
|
||||
import { registerProjectBotRouter } from "./bot-router";
|
||||
import { registerCaRouter } from "./certificate-authority-router";
|
||||
import { registerCertRouter } from "./certificate-router";
|
||||
import { registerCertificateTemplateRouter } from "./certificate-template-router";
|
||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
||||
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
|
||||
@@ -76,6 +77,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
async (pkiRouter) => {
|
||||
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
|
||||
await pkiRouter.register(registerCertRouter, { prefix: "/certificates" });
|
||||
await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
|
||||
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
|
||||
import { ProjectFilterType } from "@app/services/project/project-types";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
@@ -458,4 +459,34 @@ 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: sanitizedCertificateTemplate.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
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 };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import crypto, { KeyObject } from "crypto";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
@@ -18,6 +19,8 @@ import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns
|
||||
|
||||
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
|
||||
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
|
||||
import { validateCertificateDetailsAgainstTemplate } from "../certificate-template/certificate-template-fns";
|
||||
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||
import {
|
||||
@@ -59,6 +62,7 @@ type TCertificateAuthorityServiceFactoryDep = {
|
||||
>;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">;
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById">;
|
||||
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
|
||||
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||
@@ -76,6 +80,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateTemplateDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
pkiCollectionDAL,
|
||||
@@ -1013,6 +1018,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
*/
|
||||
const issueCertFromCa = async ({
|
||||
caId,
|
||||
certificateTemplateId,
|
||||
pkiCollectionId,
|
||||
friendlyName,
|
||||
commonName,
|
||||
@@ -1025,8 +1031,27 @@ export const certificateAuthorityServiceFactory = ({
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TIssueCertFromCaDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
let ca: TCertificateAuthorities | undefined;
|
||||
let certificateTemplate: TCertificateTemplates | undefined;
|
||||
let collectionId = pkiCollectionId;
|
||||
|
||||
if (caId) {
|
||||
ca = await certificateAuthorityDAL.findById(caId);
|
||||
} else if (certificateTemplateId) {
|
||||
certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
|
||||
if (!certificateTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found"
|
||||
});
|
||||
}
|
||||
|
||||
collectionId = certificateTemplate.pkiCollectionId as string;
|
||||
ca = await certificateAuthorityDAL.findById(certificateTemplate.caId);
|
||||
}
|
||||
|
||||
if (!ca) {
|
||||
throw new BadRequestError({ message: "CA not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@@ -1047,8 +1072,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
}
|
||||
|
||||
// check PKI collection
|
||||
if (pkiCollectionId) {
|
||||
const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId);
|
||||
if (collectionId) {
|
||||
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
|
||||
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
|
||||
if (pkiCollection.projectId !== ca.projectId) throw new BadRequestError({ message: "Invalid PKI collection" });
|
||||
}
|
||||
@@ -1121,11 +1146,13 @@ export const certificateAuthorityServiceFactory = ({
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||
];
|
||||
|
||||
let altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
}[] = [];
|
||||
|
||||
if (altNames) {
|
||||
const altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
}[] = altNames
|
||||
altNamesArray = altNames
|
||||
.split(",")
|
||||
.map((name) => name.trim())
|
||||
.map((altName) => {
|
||||
@@ -1153,6 +1180,18 @@ export const certificateAuthorityServiceFactory = ({
|
||||
extensions.push(altNamesExtension);
|
||||
}
|
||||
|
||||
if (certificateTemplate) {
|
||||
validateCertificateDetailsAgainstTemplate(
|
||||
{
|
||||
commonName,
|
||||
notBeforeDate,
|
||||
notAfterDate,
|
||||
altNames: altNamesArray.map((entry) => entry.value)
|
||||
},
|
||||
certificateTemplate
|
||||
);
|
||||
}
|
||||
|
||||
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||
const leafCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
@@ -1179,8 +1218,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
await certificateDAL.transaction(async (tx) => {
|
||||
const cert = await certificateDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
caId: (ca as TCertificateAuthorities).id,
|
||||
caCertId: caCert.id,
|
||||
certificateTemplateId: certificateTemplate?.id,
|
||||
status: CertStatus.ACTIVE,
|
||||
friendlyName: friendlyName || commonName,
|
||||
commonName,
|
||||
@@ -1200,10 +1240,10 @@ export const certificateAuthorityServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
|
||||
if (pkiCollectionId) {
|
||||
if (collectionId) {
|
||||
await pkiCollectionItemDAL.create(
|
||||
{
|
||||
pkiCollectionId,
|
||||
pkiCollectionId: collectionId,
|
||||
certId: cert.id
|
||||
},
|
||||
tx
|
||||
@@ -1237,6 +1277,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
*/
|
||||
const signCertFromCa = async ({
|
||||
caId,
|
||||
certificateTemplateId,
|
||||
csr,
|
||||
pkiCollectionId,
|
||||
friendlyName,
|
||||
@@ -1250,8 +1291,27 @@ export const certificateAuthorityServiceFactory = ({
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TSignCertFromCaDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
let ca: TCertificateAuthorities | undefined;
|
||||
let certificateTemplate: TCertificateTemplates | undefined;
|
||||
let collectionId = pkiCollectionId;
|
||||
|
||||
if (caId) {
|
||||
ca = await certificateAuthorityDAL.findById(caId);
|
||||
} else if (certificateTemplateId) {
|
||||
certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
|
||||
if (!certificateTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found"
|
||||
});
|
||||
}
|
||||
|
||||
collectionId = certificateTemplate.pkiCollectionId as string;
|
||||
ca = await certificateAuthorityDAL.findById(certificateTemplate.caId);
|
||||
}
|
||||
|
||||
if (!ca) {
|
||||
throw new BadRequestError({ message: "CA not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@@ -1346,11 +1406,12 @@ export const certificateAuthorityServiceFactory = ({
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||
];
|
||||
|
||||
let altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
}[] = [];
|
||||
if (altNames) {
|
||||
const altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
}[] = altNames
|
||||
altNamesArray = altNames
|
||||
.split(",")
|
||||
.map((name) => name.trim())
|
||||
.map((altName) => {
|
||||
@@ -1378,6 +1439,18 @@ export const certificateAuthorityServiceFactory = ({
|
||||
extensions.push(altNamesExtension);
|
||||
}
|
||||
|
||||
if (certificateTemplate) {
|
||||
validateCertificateDetailsAgainstTemplate(
|
||||
{
|
||||
commonName: cn,
|
||||
notBeforeDate,
|
||||
notAfterDate,
|
||||
altNames: altNamesArray.map((entry) => entry.value)
|
||||
},
|
||||
certificateTemplate
|
||||
);
|
||||
}
|
||||
|
||||
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||
const leafCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
@@ -1401,8 +1474,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
await certificateDAL.transaction(async (tx) => {
|
||||
const cert = await certificateDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
caId: (ca as TCertificateAuthorities).id,
|
||||
caCertId: caCert.id,
|
||||
certificateTemplateId: certificateTemplate?.id,
|
||||
status: CertStatus.ACTIVE,
|
||||
friendlyName: friendlyName || csrObj.subject,
|
||||
commonName: cn,
|
||||
@@ -1422,10 +1496,10 @@ export const certificateAuthorityServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
|
||||
if (pkiCollectionId) {
|
||||
if (collectionId) {
|
||||
await pkiCollectionItemDAL.create(
|
||||
{
|
||||
pkiCollectionId,
|
||||
pkiCollectionId: collectionId,
|
||||
certId: cert.id
|
||||
},
|
||||
tx
|
||||
|
||||
@@ -86,7 +86,8 @@ export type TImportCertToCaDTO = {
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIssueCertFromCaDTO = {
|
||||
caId: string;
|
||||
caId?: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName: string;
|
||||
@@ -97,8 +98,9 @@ export type TIssueCertFromCaDTO = {
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TSignCertFromCaDTO = {
|
||||
caId: string;
|
||||
caId?: string;
|
||||
csr: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TCertificateTemplateDALFactory = ReturnType<typeof certificateTemplateDALFactory>;
|
||||
|
||||
export const certificateTemplateDALFactory = (db: TDbClient) => {
|
||||
const certificateTemplateOrm = ormify(db, TableName.CertificateTemplate);
|
||||
|
||||
const getCertTemplatesByProjectId = async (projectId: string) => {
|
||||
try {
|
||||
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))
|
||||
.select(
|
||||
db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority),
|
||||
db.ref("projectId").withSchema(TableName.CertificateAuthority)
|
||||
);
|
||||
|
||||
return certTemplates;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Get certificate templates by project ID" });
|
||||
}
|
||||
};
|
||||
|
||||
const getById = async (id: string) => {
|
||||
try {
|
||||
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;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Get certificate template by ID" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...certificateTemplateOrm, getCertTemplatesByProjectId, getById };
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import ms from "ms";
|
||||
|
||||
import { TCertificateTemplates } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
export const validateCertificateDetailsAgainstTemplate = (
|
||||
cert: {
|
||||
commonName: string;
|
||||
notBeforeDate: Date;
|
||||
notAfterDate: Date;
|
||||
altNames: string[];
|
||||
},
|
||||
template: TCertificateTemplates
|
||||
) => {
|
||||
const commonNameRegex = new RegExp(template.commonName);
|
||||
if (!commonNameRegex.test(cert.commonName)) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid common name based on template policy"
|
||||
});
|
||||
}
|
||||
|
||||
if (cert.notAfterDate.getTime() - cert.notBeforeDate.getTime() > ms(template.ttl)) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid validity date based on template policy"
|
||||
});
|
||||
}
|
||||
|
||||
const subjectAlternativeNameRegex = new RegExp(template.subjectAlternativeName);
|
||||
cert.altNames.forEach((altName) => {
|
||||
if (!subjectAlternativeNameRegex.test(altName)) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid subject alternative name based on template policy"
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import z from "zod";
|
||||
|
||||
import { CertificateTemplatesSchema } from "@app/db/schemas";
|
||||
|
||||
export const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({
|
||||
id: true,
|
||||
caId: true,
|
||||
name: true,
|
||||
commonName: true,
|
||||
subjectAlternativeName: true,
|
||||
pkiCollectionId: true,
|
||||
ttl: true
|
||||
}).merge(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
caName: z.string()
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,196 @@
|
||||
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 { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||
import { TCertificateTemplateDALFactory } from "./certificate-template-dal";
|
||||
import {
|
||||
TCreateCertTemplateDTO,
|
||||
TDeleteCertTemplateDTO,
|
||||
TGetCertTemplateDTO,
|
||||
TUpdateCertTemplateDTO
|
||||
} from "./certificate-template-types";
|
||||
|
||||
type TCertificateTemplateServiceFactoryDep = {
|
||||
certificateTemplateDAL: TCertificateTemplateDALFactory;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
|
||||
export type TCertificateTemplateServiceFactory = ReturnType<typeof certificateTemplateServiceFactory>;
|
||||
|
||||
export const certificateTemplateServiceFactory = ({
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityDAL,
|
||||
permissionService
|
||||
}: TCertificateTemplateServiceFactoryDep) => {
|
||||
const createCertTemplate = async ({
|
||||
caId,
|
||||
pkiCollectionId,
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
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 { id } = await certificateTemplateDAL.create({
|
||||
caId,
|
||||
pkiCollectionId,
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl
|
||||
});
|
||||
|
||||
const certificateTemplate = await certificateTemplateDAL.getById(id);
|
||||
if (!certificateTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found"
|
||||
});
|
||||
}
|
||||
|
||||
return certificateTemplate;
|
||||
};
|
||||
|
||||
const updateCertTemplate = async ({
|
||||
id,
|
||||
caId,
|
||||
pkiCollectionId,
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdateCertTemplateDTO) => {
|
||||
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.Edit,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
|
||||
if (caId) {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca || ca.projectId !== certTemplate.projectId) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid CA"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await certificateTemplateDAL.updateById(certTemplate.id, {
|
||||
caId,
|
||||
pkiCollectionId,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
name,
|
||||
ttl
|
||||
});
|
||||
|
||||
const updatedTemplate = await certificateTemplateDAL.getById(id);
|
||||
if (!updatedTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found"
|
||||
});
|
||||
}
|
||||
|
||||
return updatedTemplate;
|
||||
};
|
||||
|
||||
const deleteCertTemplate = async ({ id, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertTemplateDTO) => {
|
||||
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.Delete,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
|
||||
await certificateTemplateDAL.deleteById(certTemplate.id);
|
||||
|
||||
return certTemplate;
|
||||
};
|
||||
|
||||
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,
|
||||
deleteCertTemplate,
|
||||
updateCertTemplate
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreateCertTemplateDTO = {
|
||||
caId: string;
|
||||
pkiCollectionId?: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateCertTemplateDTO = {
|
||||
id: string;
|
||||
caId?: string;
|
||||
pkiCollectionId?: string;
|
||||
name?: string;
|
||||
commonName?: string;
|
||||
subjectAlternativeName?: string;
|
||||
ttl?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCertTemplateDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteCertTemplateDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
@@ -0,0 +1,14 @@
|
||||
import safe from "safe-regex";
|
||||
import z from "zod";
|
||||
|
||||
export const validateTemplateRegexField = z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, {
|
||||
message: "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
||||
})
|
||||
// we ensure that the inputted pattern is computationally safe by limiting star height to 1
|
||||
.refine((v) => safe(v), {
|
||||
message: "Unsafe REGEX pattern"
|
||||
});
|
||||
@@ -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<TProjectUserMembershipRoleDALFactory, "create">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
||||
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
||||
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,
|
||||
|
||||
@@ -117,3 +117,5 @@ export type TLoadProjectKmsBackupDTO = {
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetProjectKmsKey = TProjectPermission;
|
||||
|
||||
export type TListProjectCertificateTemplatesDTO = TProjectPermission;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/pki/certificate-templates"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/pki/certificate-templates/{certificateTemplateId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/pki/certificate-templates/{certificateTemplateId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/pki/certificate-templates/{certificateTemplateId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Issue Certificate"
|
||||
openapi: "POST /api/v1/pki/certificates/issue-certificate"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Sign Certificate"
|
||||
openapi: "POST /api/v1/pki/certificates/sign-certificate"
|
||||
---
|
||||
111
docs/documentation/platform/pki/certificate-templates.mdx
Normal file
111
docs/documentation/platform/pki/certificate-templates.mdx
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: "Certificate Templates"
|
||||
sidebarTitle: "Certificate Templates"
|
||||
description: "Learn how to use certificate templates to enforce policies."
|
||||
---
|
||||
|
||||
## Concept
|
||||
|
||||
In order to ensure your certificates follow certain policies, you can use certificate templates during the issuance and signing flows.
|
||||
|
||||
A certificate template is linked to a certificate authority. It contains custom policies for certificate fields, allowing you to define rules based on your security policies.
|
||||
|
||||
## Workflow
|
||||
|
||||
The typical workflow for using certificate templates consists of the following steps:
|
||||
|
||||
1. Creating a certificate template attached to an existing CA along with defining custom rules for certificate fields.
|
||||
2. Selecting the certificate template during the creation of new certificates.
|
||||
|
||||
<Note>
|
||||
Note that this workflow can be executed via the Infisical UI or manually such
|
||||
as via API.
|
||||
</Note>
|
||||
|
||||
## Guide to using Certificate Templates
|
||||
|
||||
In the following steps, we explore how to issue a X.509 certificate using a certificate template.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Infisical UI">
|
||||
|
||||
<Steps>
|
||||
<Step title="Creating the certificate template">
|
||||
To create a certificate template, head to your Project > Internal PKI > Certificate Templates and press **Create Certificate Template**.
|
||||
|
||||

|
||||
|
||||
Here, set the **Issuing CA** to the CA you want to issue certificates under when the certificate template is used.
|
||||
|
||||

|
||||
|
||||
Here's some guidance on each field:
|
||||
- Template Name: A descriptive name for the certificate template.
|
||||
- Issuing CA: The Certificate Authority (CA) that will issue certificates based on this template.
|
||||
- Certificate Collection: The collection where certificates issued with this template will be added.
|
||||
- Common Name (CN): The regular expression used to validate the common name in certificate requests.
|
||||
- Alternative Names (SANs): The regular expression used to validate subject alternative names in certificate requests.
|
||||
- TTL: The maximum Time-to-Live (TTL) for certificates issued using this template.
|
||||
|
||||
</Step>
|
||||
<Step title="Using the certificate template">
|
||||
Once you have created the certificate template from step 1, you can select it when issuing certificates.
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
<Tab title="API">
|
||||
<Steps>
|
||||
<Step title="Creating the certificate template">
|
||||
To create a certificate template, make an API request to the [Create Certificate Template](/api-reference/endpoints/certificate-templates/create) API endpoint.
|
||||
|
||||
### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --request POST \
|
||||
--url https://app.infisical.com/api/v1/pki/certificate-templates \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"caId": "<string>",
|
||||
"pkiCollectionId": "<string>",
|
||||
"name": "<string>",
|
||||
"commonName": "<string>",
|
||||
"subjectAlternativeName": "<string>",
|
||||
"ttl": "<string>"
|
||||
}'
|
||||
```
|
||||
|
||||
### Sample response
|
||||
|
||||
```bash Response
|
||||
{
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"caId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"name": "certificate-template-1",
|
||||
"commonName": "<string>",
|
||||
...
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
<Step title="Using the certificate template">
|
||||
To use the certificate template, attach the certificate template ID when invoking the API endpoint for [issuing](/api-reference/endpoints/certificates/issue-certificate) or [signing](/api-reference/endpoints/certificates/sign-certificate) new certificates.
|
||||
|
||||
### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --request POST \
|
||||
--url https://app.infisical.com/api/v1/pki/certificates/issue-certificate \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"certificateTemplateId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"friendlyName": "my-new-certificate",
|
||||
"commonName": "CERT",
|
||||
...
|
||||
}'
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 715 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
@@ -108,6 +108,7 @@
|
||||
"documentation/platform/pki/overview",
|
||||
"documentation/platform/pki/private-ca",
|
||||
"documentation/platform/pki/certificates",
|
||||
"documentation/platform/pki/certificate-templates",
|
||||
"documentation/platform/pki/alerting"
|
||||
]
|
||||
},
|
||||
@@ -701,7 +702,18 @@
|
||||
"api-reference/endpoints/certificates/read",
|
||||
"api-reference/endpoints/certificates/revoke",
|
||||
"api-reference/endpoints/certificates/delete",
|
||||
"api-reference/endpoints/certificates/cert-body"
|
||||
"api-reference/endpoints/certificates/cert-body",
|
||||
"api-reference/endpoints/certificates/issue-certificate",
|
||||
"api-reference/endpoints/certificates/sign-certificate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Certificate Templates",
|
||||
"pages": [
|
||||
"api-reference/endpoints/certificate-templates/create",
|
||||
"api-reference/endpoints/certificate-templates/update",
|
||||
"api-reference/endpoints/certificate-templates/get-by-id",
|
||||
"api-reference/endpoints/certificate-templates/delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -28,6 +28,7 @@ export enum ProjectPermissionSub {
|
||||
Identity = "identity",
|
||||
CertificateAuthorities = "certificate-authorities",
|
||||
Certificates = "certificates",
|
||||
CertificateTemplates = "certificate-templates",
|
||||
PkiAlerts = "pki-alerts",
|
||||
PkiCollections = "pki-collections",
|
||||
Kms = "kms"
|
||||
@@ -59,6 +60,7 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
||||
|
||||
@@ -68,7 +68,11 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.GET_PKI_COLLECTION_ITEMS]: "Get PKI collection items",
|
||||
[EventType.ADD_PKI_COLLECTION_ITEM]: "Add PKI collection item",
|
||||
[EventType.DELETE_PKI_COLLECTION_ITEM]: "Delete PKI collection item",
|
||||
[EventType.ORG_ADMIN_ACCESS_PROJECT]: "Org admin accessed project"
|
||||
[EventType.ORG_ADMIN_ACCESS_PROJECT]: "Org admin accessed project",
|
||||
[EventType.CREATE_CERTIFICATE_TEMPLATE]: "Create certificate template",
|
||||
[EventType.UPDATE_CERTIFICATE_TEMPLATE]: "Update certificate template",
|
||||
[EventType.DELETE_CERTIFICATE_TEMPLATE]: "Delete certificate template",
|
||||
[EventType.GET_CERTIFICATE_TEMPLATE]: "Get certificate template"
|
||||
};
|
||||
|
||||
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||
|
||||
@@ -82,5 +82,9 @@ export enum EventType {
|
||||
GET_PKI_COLLECTION_ITEMS = "get-pki-collection-items",
|
||||
ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item",
|
||||
DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item",
|
||||
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project"
|
||||
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
|
||||
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
||||
GET_CERTIFICATE_TEMPLATE = "get-certificate-template"
|
||||
}
|
||||
|
||||
@@ -679,6 +679,46 @@ interface OrgAdminAccessProjectEvent {
|
||||
}; // no metadata yet
|
||||
}
|
||||
|
||||
interface CreateCertificateTemplate {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
caId: string;
|
||||
pkiCollectionId?: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCertificateTemplate {
|
||||
type: EventType.GET_CERTIFICATE_TEMPLATE;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateCertificateTemplate {
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
caId: string;
|
||||
pkiCollectionId?: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteCertificateTemplate {
|
||||
type: EventType.DELETE_CERTIFICATE_TEMPLATE;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -747,7 +787,11 @@ export type Event =
|
||||
| GetPkiCollectionItems
|
||||
| AddPkiCollectionItem
|
||||
| DeletePkiCollectionItem
|
||||
| OrgAdminAccessProjectEvent;
|
||||
| OrgAdminAccessProjectEvent
|
||||
| CreateCertificateTemplate
|
||||
| UpdateCertificateTemplate
|
||||
| GetCertificateTemplate
|
||||
| DeleteCertificateTemplate;
|
||||
|
||||
export type AuditLog = {
|
||||
id: string;
|
||||
|
||||
@@ -99,9 +99,9 @@ export const useImportCaCertificate = () => {
|
||||
export const useCreateCertificate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCreateCertificateResponse, {}, TCreateCertificateDTO>({
|
||||
mutationFn: async ({ caId, ...body }) => {
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post<TCreateCertificateResponse>(
|
||||
`/api/v1/pki/ca/${caId}/issue-certificate`,
|
||||
"/api/v1/pki/certificates/issue-certificate",
|
||||
body
|
||||
);
|
||||
return data;
|
||||
|
||||
@@ -79,7 +79,8 @@ export type TImportCaCertificateResponse = {
|
||||
|
||||
export type TCreateCertificateDTO = {
|
||||
projectSlug: string;
|
||||
caId: string;
|
||||
caId?: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName: string;
|
||||
|
||||
2
frontend/src/hooks/api/certificateTemplates/index.tsx
Normal file
2
frontend/src/hooks/api/certificateTemplates/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { useCreateCertTemplate, useDeleteCertTemplate, useUpdateCertTemplate } from "./mutations";
|
||||
export { useGetCertTemplate } from "./queries";
|
||||
59
frontend/src/hooks/api/certificateTemplates/mutations.tsx
Normal file
59
frontend/src/hooks/api/certificateTemplates/mutations.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import { certTemplateKeys } from "./queries";
|
||||
import {
|
||||
TCertificateTemplate,
|
||||
TCreateCertificateTemplateDTO,
|
||||
TDeleteCertificateTemplateDTO,
|
||||
TUpdateCertificateTemplateDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateCertTemplate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateTemplate, {}, TCreateCertificateTemplateDTO>({
|
||||
mutationFn: async (data) => {
|
||||
const { data: certificateTemplate } = await apiRequest.post<TCertificateTemplate>(
|
||||
"/api/v1/pki/certificate-templates",
|
||||
data
|
||||
);
|
||||
return certificateTemplate;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateCertTemplate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateTemplate, {}, TUpdateCertificateTemplateDTO>({
|
||||
mutationFn: async (data) => {
|
||||
const { data: certificateTemplate } = await apiRequest.patch<TCertificateTemplate>(
|
||||
`/api/v1/pki/certificate-templates/${data.id}`,
|
||||
data
|
||||
);
|
||||
|
||||
return certificateTemplate;
|
||||
},
|
||||
onSuccess: (_, { projectId, id }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
|
||||
queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteCertTemplate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, {}, TDeleteCertificateTemplateDTO>({
|
||||
mutationFn: async (data) => {
|
||||
return apiRequest.delete(`/api/v1/pki/certificate-templates/${data.id}`);
|
||||
},
|
||||
onSuccess: (_, { projectId, id }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
|
||||
queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id));
|
||||
}
|
||||
});
|
||||
};
|
||||
22
frontend/src/hooks/api/certificateTemplates/queries.tsx
Normal file
22
frontend/src/hooks/api/certificateTemplates/queries.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TCertificateTemplate } from "./types";
|
||||
|
||||
export const certTemplateKeys = {
|
||||
getCertTemplateById: (id: string) => [{ id }, "cert-template"]
|
||||
};
|
||||
|
||||
export const useGetCertTemplate = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: certTemplateKeys.getCertTemplateById(id),
|
||||
queryFn: async () => {
|
||||
const { data: certificateTemplate } = await apiRequest.get<TCertificateTemplate>(
|
||||
`/api/v1/pki/certificate-templates/${id}`
|
||||
);
|
||||
return certificateTemplate;
|
||||
},
|
||||
enabled: Boolean(id)
|
||||
});
|
||||
};
|
||||
37
frontend/src/hooks/api/certificateTemplates/types.ts
Normal file
37
frontend/src/hooks/api/certificateTemplates/types.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export type TCertificateTemplate = {
|
||||
id: string;
|
||||
caId: string;
|
||||
caName: string;
|
||||
projectId: string;
|
||||
pkiCollectionId?: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
};
|
||||
|
||||
export type TCreateCertificateTemplateDTO = {
|
||||
caId: string;
|
||||
pkiCollectionId?: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TUpdateCertificateTemplateDTO = {
|
||||
id: string;
|
||||
caId?: string;
|
||||
pkiCollectionId?: string;
|
||||
name?: string;
|
||||
commonName?: string;
|
||||
subjectAlternativeName?: string;
|
||||
ttl?: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TDeleteCertificateTemplateDTO = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { CertStatus } from "./enums";
|
||||
export type TCertificate = {
|
||||
id: string;
|
||||
caId: string;
|
||||
certificateTemplateId?: string;
|
||||
status: CertStatus;
|
||||
friendlyName: string;
|
||||
commonName: string;
|
||||
|
||||
@@ -7,6 +7,7 @@ export * from "./auth";
|
||||
export * from "./bots";
|
||||
export * from "./ca";
|
||||
export * from "./certificates";
|
||||
export * from "./certificateTemplates";
|
||||
export * from "./dynamicSecret";
|
||||
export * from "./dynamicSecretLease";
|
||||
export * from "./groups";
|
||||
|
||||
@@ -25,6 +25,7 @@ export {
|
||||
useGetWorkspaceUsers,
|
||||
useListWorkspaceCas,
|
||||
useListWorkspaceCertificates,
|
||||
useListWorkspaceCertificateTemplates,
|
||||
useListWorkspaceGroups,
|
||||
useListWorkspacePkiAlerts,
|
||||
useListWorkspacePkiCollections,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { apiRequest } from "@app/config/request";
|
||||
import { CaStatus } from "../ca/enums";
|
||||
import { TCertificateAuthority } from "../ca/types";
|
||||
import { TCertificate } from "../certificates/types";
|
||||
import { TCertificateTemplate } from "../certificateTemplates/types";
|
||||
import { TGroupMembership } from "../groups/types";
|
||||
import { identitiesKeys } from "../identities/queries";
|
||||
import { IdentityMembership } from "../identities/types";
|
||||
@@ -68,7 +69,9 @@ export const workspaceKeys = {
|
||||
getWorkspacePkiAlerts: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-pki-alerts"] as const,
|
||||
getWorkspacePkiCollections: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-pki-collections"] as const
|
||||
[{ workspaceId }, "workspace-pki-collections"] as const,
|
||||
getWorkspaceCertificateTemplates: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-certificate-templates"] as const
|
||||
};
|
||||
|
||||
const fetchWorkspaceById = async (workspaceId: string) => {
|
||||
@@ -639,3 +642,19 @@ export const useListWorkspacePkiCollections = ({ workspaceId }: { workspaceId: s
|
||||
enabled: Boolean(workspaceId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useListWorkspaceCertificateTemplates = ({ workspaceId }: { workspaceId: string }) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getWorkspaceCertificateTemplates(workspaceId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { certificateTemplates }
|
||||
} = await apiRequest.get<{ certificateTemplates: TCertificateTemplate[] }>(
|
||||
`/api/v2/workspace/${workspaceId}/certificate-templates`
|
||||
);
|
||||
|
||||
return { certificateTemplates };
|
||||
},
|
||||
enabled: Boolean(workspaceId)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -406,6 +406,28 @@ export const LogsTableRow = ({ auditLog }: Props) => {
|
||||
<p>{`Cert CN: ${event.metadata.cn}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_CERTIFICATE_TEMPLATE:
|
||||
case EventType.UPDATE_CERTIFICATE_TEMPLATE:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
<p>{`Certificate Authority ID: ${event.metadata.caId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
<p>{`Common Name: ${event.metadata.commonName}`}</p>
|
||||
<p>{`Subject Alternative Name: ${event.metadata.subjectAlternativeName}`}</p>
|
||||
<p>{`TTL: ${event.metadata.ttl}`}</p>
|
||||
{event.metadata.pkiCollectionId && (
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
)}
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_CERTIFICATE_TEMPLATE:
|
||||
case EventType.DELETE_CERTIFICATE_TEMPLATE:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
default:
|
||||
return <Td />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { PkiCollectionSection } from "../PkiAlertsTab/components";
|
||||
import { CertificateTemplatesSection } from "./components/CertificateTemplatesSection";
|
||||
import { CertificatesSection } from "./components";
|
||||
|
||||
export const CertificatesTab = () => {
|
||||
@@ -13,6 +14,7 @@ export const CertificatesTab = () => {
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<PkiCollectionSection />
|
||||
<CertificateTemplatesSection />
|
||||
<CertificatesSection />
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -7,18 +9,22 @@ import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
SelectItem,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
CaStatus,
|
||||
useCreateCertificate,
|
||||
useGetCert,
|
||||
useGetCertTemplate,
|
||||
useListWorkspaceCas,
|
||||
useListWorkspaceCertificateTemplates,
|
||||
useListWorkspacePkiCollections
|
||||
} from "@app/hooks/api";
|
||||
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||
@@ -27,6 +33,7 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { CertificateContent } from "./CertificateContent";
|
||||
|
||||
const schema = z.object({
|
||||
certificateTemplateId: z.string().optional(),
|
||||
caId: z.string(),
|
||||
collectionId: z.string().optional(),
|
||||
friendlyName: z.string(),
|
||||
@@ -49,6 +56,8 @@ type TCertificateDetails = {
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
const CERT_TEMPLATE_NONE_VALUE = "none";
|
||||
|
||||
export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
@@ -65,6 +74,10 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
workspaceId: currentWorkspace?.id || ""
|
||||
});
|
||||
|
||||
const { data: templatesData } = useListWorkspaceCertificateTemplates({
|
||||
workspaceId: currentWorkspace?.id || ""
|
||||
});
|
||||
|
||||
const { mutateAsync: createCertificate } = useCreateCertificate();
|
||||
|
||||
const {
|
||||
@@ -72,11 +85,20 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
setValue
|
||||
setValue,
|
||||
watch
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
const selectedCertTemplateId = watch("certificateTemplateId");
|
||||
const hasCertTemplateSelected =
|
||||
selectedCertTemplateId !== "" && selectedCertTemplateId !== CERT_TEMPLATE_NONE_VALUE;
|
||||
|
||||
const { data: selectedCertTemplate } = useGetCertTemplate(
|
||||
hasCertTemplateSelected ? (selectedCertTemplateId as string) : ""
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (cert) {
|
||||
reset({
|
||||
@@ -84,6 +106,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
friendlyName: cert.friendlyName,
|
||||
commonName: cert.commonName,
|
||||
altNames: cert.altNames,
|
||||
certificateTemplateId: cert.certificateTemplateId ?? CERT_TEMPLATE_NONE_VALUE,
|
||||
ttl: ""
|
||||
});
|
||||
} else {
|
||||
@@ -92,15 +115,22 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
friendlyName: "",
|
||||
commonName: "",
|
||||
altNames: "",
|
||||
ttl: ""
|
||||
ttl: "",
|
||||
certificateTemplateId: CERT_TEMPLATE_NONE_VALUE
|
||||
});
|
||||
}
|
||||
}, [cert]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cert && selectedCertTemplate) {
|
||||
setValue("ttl", selectedCertTemplate.ttl);
|
||||
}
|
||||
}, [selectedCertTemplate, cert]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
caId,
|
||||
collectionId,
|
||||
friendlyName,
|
||||
collectionId,
|
||||
commonName,
|
||||
altNames,
|
||||
ttl
|
||||
@@ -109,8 +139,9 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({
|
||||
caId: !selectedCertTemplate ? caId : undefined,
|
||||
certificateTemplateId: selectedCertTemplate ? selectedCertTemplateId : undefined,
|
||||
projectSlug: currentWorkspace.slug,
|
||||
caId,
|
||||
pkiCollectionId: collectionId,
|
||||
friendlyName,
|
||||
commonName,
|
||||
@@ -160,11 +191,31 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caId"
|
||||
name="certificateTemplateId"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Issuing CA"
|
||||
label={
|
||||
<div>
|
||||
<FormLabel
|
||||
isRequired
|
||||
label="Certificate Template"
|
||||
icon={
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={
|
||||
<span>
|
||||
When a template is selected, the details provided are validated
|
||||
against the template policies.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
@@ -177,42 +228,76 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
className="w-full"
|
||||
isDisabled={Boolean(cert)}
|
||||
>
|
||||
{(cas || []).map(({ id, type, dn }) => (
|
||||
<SelectItem value={id} key={`ca-${id}`}>
|
||||
{`${caTypeToNameMap[type]}: ${dn}`}
|
||||
<SelectItem value={CERT_TEMPLATE_NONE_VALUE} key="cert-template-none">
|
||||
None
|
||||
</SelectItem>
|
||||
{(templatesData?.certificateTemplates || []).map(({ id, name }) => (
|
||||
<SelectItem value={id} key={`cert-template-${id}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!cert && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="collectionId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Certificate Collection (Optional)"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(cert)}
|
||||
{(!selectedCertTemplateId ||
|
||||
selectedCertTemplateId === CERT_TEMPLATE_NONE_VALUE ||
|
||||
cert) && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Issuing CA"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
isRequired
|
||||
>
|
||||
{(data?.collections || []).map(({ id, name }) => (
|
||||
<SelectItem value={id} key={`pki-collection-${id}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(cert)}
|
||||
>
|
||||
{(cas || []).map(({ id, type, dn }) => (
|
||||
<SelectItem value={id} key={`ca-${id}`}>
|
||||
{`${caTypeToNameMap[type]}: ${dn}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="collectionId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Certificate Collection (Optional)"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(cert)}
|
||||
>
|
||||
{(data?.collections || []).map(({ id, name }) => (
|
||||
<SelectItem value={id} key={`pki-collection-${id}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
CaStatus,
|
||||
useCreateCertTemplate,
|
||||
useGetCertTemplate,
|
||||
useListWorkspaceCas,
|
||||
useListWorkspacePkiCollections,
|
||||
useUpdateCertTemplate
|
||||
} from "@app/hooks/api";
|
||||
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const validateTemplateRegexField = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, {
|
||||
message:
|
||||
"Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
caId: z.string(),
|
||||
collectionId: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
commonName: validateTemplateRegexField,
|
||||
subjectAlternativeName: validateTemplateRegexField,
|
||||
ttl: z.string().trim().min(1)
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["certificateTemplate"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["certificateTemplate"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: certTemplate } = useGetCertTemplate(
|
||||
(popUp?.certificateTemplate?.data as { id: string })?.id || ""
|
||||
);
|
||||
|
||||
const { data: cas } = useListWorkspaceCas({
|
||||
projectSlug: currentWorkspace?.slug ?? "",
|
||||
status: CaStatus.ACTIVE
|
||||
});
|
||||
|
||||
const { data: collectionsData } = useListWorkspacePkiCollections({
|
||||
workspaceId: currentWorkspace?.id || ""
|
||||
});
|
||||
|
||||
const { mutateAsync: createCertTemplate } = useCreateCertTemplate();
|
||||
const { mutateAsync: updateCertTemplate } = useUpdateCertTemplate();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (certTemplate) {
|
||||
reset({
|
||||
caId: certTemplate.caId,
|
||||
name: certTemplate.name,
|
||||
commonName: certTemplate.commonName,
|
||||
subjectAlternativeName: certTemplate.subjectAlternativeName,
|
||||
collectionId: certTemplate.pkiCollectionId ?? undefined,
|
||||
ttl: certTemplate.ttl
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
caId: "",
|
||||
name: "",
|
||||
commonName: "",
|
||||
ttl: ""
|
||||
});
|
||||
}
|
||||
}, [certTemplate]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
caId,
|
||||
collectionId,
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl
|
||||
}: FormData) => {
|
||||
if (!currentWorkspace?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (certTemplate) {
|
||||
await updateCertTemplate({
|
||||
id: certTemplate.id,
|
||||
projectId: currentWorkspace.id,
|
||||
pkiCollectionId: collectionId,
|
||||
caId,
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated certificate template",
|
||||
type: "success"
|
||||
});
|
||||
} else {
|
||||
await createCertTemplate({
|
||||
projectId: currentWorkspace.id,
|
||||
pkiCollectionId: collectionId,
|
||||
caId,
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created certificate template",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
|
||||
reset();
|
||||
handlePopUpToggle("certificateTemplate", false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to save changes",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.certificateTemplate?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("certificateTemplate", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title={certTemplate ? "Certificate Template" : "Create Certificate Template"}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Template Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="My Certificate Template" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caId"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Issuing CA"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{(cas || []).map(({ id, type, dn }) => (
|
||||
<SelectItem value={id} key={`ca-${id}`}>
|
||||
{`${caTypeToNameMap[type]}: ${dn}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="collectionId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Certificate Collection (Optional)"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{(collectionsData?.collections || []).map(({ id, name }) => (
|
||||
<SelectItem value={id} key={`pki-collection-${id}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="commonName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={
|
||||
<div>
|
||||
<FormLabel
|
||||
isRequired
|
||||
label="Common Name (CN)"
|
||||
icon={
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={
|
||||
<span>
|
||||
This field accepts limited regular expressions: spaces, *, ., @, -, \
|
||||
(for escaping), and alphanumeric characters only
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder=".*\.acme.com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="subjectAlternativeName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={
|
||||
<div>
|
||||
<FormLabel
|
||||
isRequired
|
||||
label="Alternative Names (SAN)"
|
||||
icon={
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={
|
||||
<span>
|
||||
This field accepts limited regular expressions: spaces, *, ., @, -, \
|
||||
(for escaping), and alphanumeric characters only
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="service\.acme.\..*" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="ttl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Max TTL"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("certificateTemplate", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteCertTemplate } from "@app/hooks/api";
|
||||
|
||||
import { CertificateTemplateModal } from "./CertificateTemplateModal";
|
||||
import { CertificateTemplatesTable } from "./CertificateTemplatesTable";
|
||||
|
||||
export const CertificateTemplatesSection = () => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"certificateTemplate",
|
||||
"deleteCertificateTemplate"
|
||||
] as const);
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync: deleteCertTemplate } = useDeleteCertTemplate();
|
||||
|
||||
const onRemoveCertificateTemplateSubmit = async (id: string) => {
|
||||
if (!currentWorkspace?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteCertTemplate({
|
||||
id,
|
||||
projectId: currentWorkspace.id
|
||||
});
|
||||
|
||||
await createNotification({
|
||||
text: "Successfully deleted certificate template",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteCertificateTemplate");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete certificate template",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Certificate Templates</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("certificateTemplate")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<CertificateTemplatesTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<CertificateTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteCertificateTemplate.isOpen}
|
||||
title={`Are you sure want to delete the certificate template ${
|
||||
(popUp?.deleteCertificateTemplate?.data as { name: string })?.name || ""
|
||||
} from the project?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteCertificateTemplate", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onRemoveCertificateTemplateSubmit(
|
||||
(popUp?.deleteCertificateTemplate?.data as { id: string })?.id
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
import { faEllipsis, faFileAlt, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useListWorkspaceCertificateTemplates } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["certificateTemplate", "deleteCertificateTemplate"]>,
|
||||
data?: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isLoading } = useListWorkspaceCertificateTemplates({
|
||||
workspaceId: currentWorkspace?.id ?? ""
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Certificate Authority</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={2} innerKey="project-cas" />}
|
||||
{!isLoading &&
|
||||
data?.certificateTemplates.map((certificateTemplate) => {
|
||||
return (
|
||||
<Tr className="h-10" key={`certificate-${certificateTemplate.id}`}>
|
||||
<Td>{certificateTemplate.name}</Td>
|
||||
<Td>{certificateTemplate.caName}</Td>
|
||||
<Td className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<Tooltip content="More options">
|
||||
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
handlePopUpOpen("certificateTemplate", {
|
||||
id: certificateTemplate.id
|
||||
})
|
||||
}
|
||||
icon={<FontAwesomeIcon icon={faFileAlt} />}
|
||||
>
|
||||
Manage Policies
|
||||
</DropdownMenuItem>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.CertificateTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteCertificateTemplate", {
|
||||
id: certificateTemplate.id,
|
||||
name: certificateTemplate.name
|
||||
})
|
||||
}
|
||||
>
|
||||
Delete Template
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isLoading && !data?.certificateTemplates?.length && (
|
||||
<EmptyState title="No certificate templates have been created" icon={faFileAlt} />
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -61,7 +61,7 @@ export const CertificatesSection = () => {
|
||||
onClick={() => handlePopUpOpen("certificate")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Issue Certificate
|
||||
Issue
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
@@ -209,7 +209,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
/>
|
||||
)}
|
||||
{!isLoading && !data?.certificates?.length && (
|
||||
<EmptyState title="No certificates have been created" icon={faCertificate} />
|
||||
<EmptyState title="No certificates have been issued" icon={faCertificate} />
|
||||
)}
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
@@ -53,6 +53,7 @@ export const formSchema = z.object({
|
||||
certificates: generalPermissionSchema,
|
||||
"pki-alerts": generalPermissionSchema,
|
||||
"pki-collections": generalPermissionSchema,
|
||||
"certificate-templates": generalPermissionSchema,
|
||||
// akhilmhdh: refactor all keys like below
|
||||
[ProjectPermissionSub.SecretApproval]: generalPermissionSchema,
|
||||
workspace: z
|
||||
|
||||
@@ -81,6 +81,10 @@ const SINGLE_PERMISSION_LIST = [
|
||||
title: "Certificates",
|
||||
formName: "certificates"
|
||||
},
|
||||
{
|
||||
title: "Certificate Templates",
|
||||
formName: "certificate-templates"
|
||||
},
|
||||
{
|
||||
title: "PKI Collections",
|
||||
formName: "pki-collections"
|
||||
|
||||
Reference in New Issue
Block a user