mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 10:28:21 +00:00
Merge pull request #2291 from Infisical/feature/certificate-template
feat: certificate templates
This commit is contained in:
Generated
+24
@@ -76,6 +76,7 @@
|
|||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
"posthog-node": "^3.6.2",
|
"posthog-node": "^3.6.2",
|
||||||
"probot": "^13.0.0",
|
"probot": "^13.0.0",
|
||||||
|
"safe-regex": "^2.1.1",
|
||||||
"smee-client": "^2.0.0",
|
"smee-client": "^2.0.0",
|
||||||
"tedious": "^18.2.1",
|
"tedious": "^18.2.1",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
"@types/picomatch": "^2.3.3",
|
"@types/picomatch": "^2.3.3",
|
||||||
"@types/prompt-sync": "^4.2.3",
|
"@types/prompt-sync": "^4.2.3",
|
||||||
"@types/resolve": "^1.20.6",
|
"@types/resolve": "^1.20.6",
|
||||||
|
"@types/safe-regex": "^1.1.6",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||||
"@typescript-eslint/parser": "^6.20.0",
|
"@typescript-eslint/parser": "^6.20.0",
|
||||||
@@ -9801,6 +9803,12 @@
|
|||||||
"integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==",
|
"integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/semver": {
|
||||||
"version": "7.5.6",
|
"version": "7.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
||||||
@@ -17821,6 +17829,14 @@
|
|||||||
"@babel/runtime": "^7.8.4"
|
"@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": {
|
"node_modules/regexp.prototype.flags": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
|
"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": {
|
"node_modules/safe-regex-test": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
|
"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/picomatch": "^2.3.3",
|
||||||
"@types/prompt-sync": "^4.2.3",
|
"@types/prompt-sync": "^4.2.3",
|
||||||
"@types/resolve": "^1.20.6",
|
"@types/resolve": "^1.20.6",
|
||||||
|
"@types/safe-regex": "^1.1.6",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||||
"@typescript-eslint/parser": "^6.20.0",
|
"@typescript-eslint/parser": "^6.20.0",
|
||||||
@@ -172,6 +173,7 @@
|
|||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
"posthog-node": "^3.6.2",
|
"posthog-node": "^3.6.2",
|
||||||
"probot": "^13.0.0",
|
"probot": "^13.0.0",
|
||||||
|
"safe-regex": "^2.1.1",
|
||||||
"smee-client": "^2.0.0",
|
"smee-client": "^2.0.0",
|
||||||
"tedious": "^18.2.1",
|
"tedious": "^18.2.1",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
|
|||||||
Vendored
+2
@@ -36,6 +36,7 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
|||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-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 { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||||
@@ -156,6 +157,7 @@ declare module "fastify" {
|
|||||||
auditLog: TAuditLogServiceFactory;
|
auditLog: TAuditLogServiceFactory;
|
||||||
auditLogStream: TAuditLogStreamServiceFactory;
|
auditLogStream: TAuditLogStreamServiceFactory;
|
||||||
certificate: TCertificateServiceFactory;
|
certificate: TCertificateServiceFactory;
|
||||||
|
certificateTemplate: TCertificateTemplateServiceFactory;
|
||||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||||
pkiCollection: TPkiCollectionServiceFactory;
|
pkiCollection: TPkiCollectionServiceFactory;
|
||||||
|
|||||||
Vendored
+8
@@ -53,6 +53,9 @@ import {
|
|||||||
TCertificateSecretsUpdate,
|
TCertificateSecretsUpdate,
|
||||||
TCertificatesInsert,
|
TCertificatesInsert,
|
||||||
TCertificatesUpdate,
|
TCertificatesUpdate,
|
||||||
|
TCertificateTemplates,
|
||||||
|
TCertificateTemplatesInsert,
|
||||||
|
TCertificateTemplatesUpdate,
|
||||||
TDynamicSecretLeases,
|
TDynamicSecretLeases,
|
||||||
TDynamicSecretLeasesInsert,
|
TDynamicSecretLeasesInsert,
|
||||||
TDynamicSecretLeasesUpdate,
|
TDynamicSecretLeasesUpdate,
|
||||||
@@ -364,6 +367,11 @@ declare module "knex/types/tables" {
|
|||||||
TCertificateAuthorityCrlUpdate
|
TCertificateAuthorityCrlUpdate
|
||||||
>;
|
>;
|
||||||
[TableName.Certificate]: KnexOriginal.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>;
|
[TableName.Certificate]: KnexOriginal.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>;
|
||||||
|
[TableName.CertificateTemplate]: KnexOriginal.CompositeTableType<
|
||||||
|
TCertificateTemplates,
|
||||||
|
TCertificateTemplatesInsert,
|
||||||
|
TCertificateTemplatesUpdate
|
||||||
|
>;
|
||||||
[TableName.CertificateBody]: KnexOriginal.CompositeTableType<
|
[TableName.CertificateBody]: KnexOriginal.CompositeTableType<
|
||||||
TCertificateBodies,
|
TCertificateBodies,
|
||||||
TCertificateBodiesInsert,
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
revokedAt: z.date().nullable().optional(),
|
||||||
revocationReason: z.number().nullable().optional(),
|
revocationReason: z.number().nullable().optional(),
|
||||||
altNames: z.string().default("").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>;
|
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export * from "./certificate-authority-crl";
|
|||||||
export * from "./certificate-authority-secret";
|
export * from "./certificate-authority-secret";
|
||||||
export * from "./certificate-bodies";
|
export * from "./certificate-bodies";
|
||||||
export * from "./certificate-secrets";
|
export * from "./certificate-secrets";
|
||||||
|
export * from "./certificate-templates";
|
||||||
export * from "./certificates";
|
export * from "./certificates";
|
||||||
export * from "./dynamic-secret-leases";
|
export * from "./dynamic-secret-leases";
|
||||||
export * from "./dynamic-secrets";
|
export * from "./dynamic-secrets";
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export enum TableName {
|
|||||||
Certificate = "certificates",
|
Certificate = "certificates",
|
||||||
CertificateBody = "certificate_bodies",
|
CertificateBody = "certificate_bodies",
|
||||||
CertificateSecret = "certificate_secrets",
|
CertificateSecret = "certificate_secrets",
|
||||||
|
CertificateTemplate = "certificate_templates",
|
||||||
PkiAlert = "pki_alerts",
|
PkiAlert = "pki_alerts",
|
||||||
PkiCollection = "pki_collections",
|
PkiCollection = "pki_collections",
|
||||||
PkiCollectionItem = "pki_collection_items",
|
PkiCollectionItem = "pki_collection_items",
|
||||||
|
|||||||
@@ -162,7 +162,11 @@ export enum EventType {
|
|||||||
UPDATE_PROJECT_KMS = "update-project-kms",
|
UPDATE_PROJECT_KMS = "update-project-kms",
|
||||||
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
|
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
|
||||||
LOAD_PROJECT_KMS_BACKUP = "load-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 {
|
interface UserActorMetadata {
|
||||||
@@ -1366,6 +1370,46 @@ interface LoadProjectKmsBackupEvent {
|
|||||||
metadata: Record<string, string>; // no metadata yet
|
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 {
|
interface OrgAdminAccessProjectEvent {
|
||||||
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
|
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -1499,4 +1543,8 @@ export type Event =
|
|||||||
| UpdateProjectKmsEvent
|
| UpdateProjectKmsEvent
|
||||||
| GetProjectKmsBackupEvent
|
| GetProjectKmsBackupEvent
|
||||||
| LoadProjectKmsBackupEvent
|
| LoadProjectKmsBackupEvent
|
||||||
| OrgAdminAccessProjectEvent;
|
| OrgAdminAccessProjectEvent
|
||||||
|
| CreateCertificateTemplate
|
||||||
|
| UpdateCertificateTemplate
|
||||||
|
| GetCertificateTemplate
|
||||||
|
| DeleteCertificateTemplate;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export enum ProjectPermissionSub {
|
|||||||
Identity = "identity",
|
Identity = "identity",
|
||||||
CertificateAuthorities = "certificate-authorities",
|
CertificateAuthorities = "certificate-authorities",
|
||||||
Certificates = "certificates",
|
Certificates = "certificates",
|
||||||
|
CertificateTemplates = "certificate-templates",
|
||||||
PkiAlerts = "pki-alerts",
|
PkiAlerts = "pki-alerts",
|
||||||
PkiCollections = "pki-collections",
|
PkiCollections = "pki-collections",
|
||||||
Kms = "kms"
|
Kms = "kms"
|
||||||
@@ -65,6 +66,7 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
|
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||||
@@ -165,6 +167,11 @@ const buildAdminPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
|
||||||
can(ProjectPermissionActions.Delete, 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.Read, ProjectPermissionSub.PkiAlerts);
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts);
|
can(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts);
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts);
|
||||||
@@ -251,6 +258,8 @@ const buildMemberPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
|
||||||
|
|
||||||
|
|||||||
@@ -1089,6 +1089,7 @@ export const CERTIFICATE_AUTHORITIES = {
|
|||||||
},
|
},
|
||||||
ISSUE_CERT: {
|
ISSUE_CERT: {
|
||||||
caId: "The ID of the CA to issue the certificate from",
|
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",
|
pkiCollectionId: "The ID of the PKI collection to add the certificate to",
|
||||||
friendlyName: "A friendly name for the certificate",
|
friendlyName: "A friendly name for the certificate",
|
||||||
commonName: "The common name (CN) 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 = {
|
export const ALERTS = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
projectId: "The ID of the project to create the alert in",
|
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 { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
|
||||||
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||||
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
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 { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
||||||
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
@@ -587,6 +589,7 @@ export const registerRoutes = async (
|
|||||||
const certificateAuthorityCertDAL = certificateAuthorityCertDALFactory(db);
|
const certificateAuthorityCertDAL = certificateAuthorityCertDALFactory(db);
|
||||||
const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
|
const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
|
||||||
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
|
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
|
||||||
|
const certificateTemplateDAL = certificateTemplateDALFactory(db);
|
||||||
|
|
||||||
const certificateDAL = certificateDALFactory(db);
|
const certificateDAL = certificateDALFactory(db);
|
||||||
const certificateBodyDAL = certificateBodyDALFactory(db);
|
const certificateBodyDAL = certificateBodyDALFactory(db);
|
||||||
@@ -622,6 +625,7 @@ export const registerRoutes = async (
|
|||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
certificateAuthoritySecretDAL,
|
certificateAuthoritySecretDAL,
|
||||||
certificateAuthorityCrlDAL,
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateTemplateDAL,
|
||||||
certificateAuthorityQueue,
|
certificateAuthorityQueue,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
@@ -641,6 +645,12 @@ export const registerRoutes = async (
|
|||||||
licenseService
|
licenseService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const certificateTemplateService = certificateTemplateServiceFactory({
|
||||||
|
certificateTemplateDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
const pkiAlertService = pkiAlertServiceFactory({
|
const pkiAlertService = pkiAlertServiceFactory({
|
||||||
pkiAlertDAL,
|
pkiAlertDAL,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
@@ -678,7 +688,8 @@ export const registerRoutes = async (
|
|||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectBotDAL
|
projectBotDAL,
|
||||||
|
certificateTemplateDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectEnvService = projectEnvServiceFactory({
|
const projectEnvService = projectEnvServiceFactory({
|
||||||
@@ -1160,6 +1171,7 @@ export const registerRoutes = async (
|
|||||||
auditLogStream: auditLogStreamService,
|
auditLogStream: auditLogStreamService,
|
||||||
certificate: certificateService,
|
certificate: certificateService,
|
||||||
certificateAuthority: certificateAuthorityService,
|
certificateAuthority: certificateAuthorityService,
|
||||||
|
certificateTemplate: certificateTemplateService,
|
||||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||||
pkiAlert: pkiAlertService,
|
pkiAlert: pkiAlertService,
|
||||||
pkiCollection: pkiCollectionService,
|
pkiCollection: pkiCollectionService,
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
|
import ms from "ms";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { CertificatesSchema } from "@app/db/schemas";
|
import { CertificatesSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { CrlReason } from "@app/services/certificate/certificate-types";
|
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) => {
|
export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
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({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:serialNumber/revoke",
|
url: "/:serialNumber/revoke",
|
||||||
|
|||||||
@@ -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 { registerProjectBotRouter } from "./bot-router";
|
||||||
import { registerCaRouter } from "./certificate-authority-router";
|
import { registerCaRouter } from "./certificate-authority-router";
|
||||||
import { registerCertRouter } from "./certificate-router";
|
import { registerCertRouter } from "./certificate-router";
|
||||||
|
import { registerCertificateTemplateRouter } from "./certificate-template-router";
|
||||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||||
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
||||||
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
|
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
|
||||||
@@ -76,6 +77,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
async (pkiRouter) => {
|
async (pkiRouter) => {
|
||||||
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
|
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
|
||||||
await pkiRouter.register(registerCertRouter, { prefix: "/certificates" });
|
await pkiRouter.register(registerCertRouter, { prefix: "/certificates" });
|
||||||
|
await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||||
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
|
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
|
||||||
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
|
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
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 { ProjectFilterType } from "@app/services/project/project-types";
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
@@ -458,4 +459,34 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
return { collections: pkiCollections };
|
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 ms from "ms";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
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 { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
|
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 { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||||
import {
|
import {
|
||||||
@@ -59,6 +62,7 @@ type TCertificateAuthorityServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">;
|
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">;
|
||||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">;
|
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">;
|
||||||
|
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById">;
|
||||||
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
|
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
|
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
|
||||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
@@ -76,6 +80,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
certificateAuthoritySecretDAL,
|
certificateAuthoritySecretDAL,
|
||||||
certificateAuthorityCrlDAL,
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateTemplateDAL,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
@@ -1013,6 +1018,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
*/
|
*/
|
||||||
const issueCertFromCa = async ({
|
const issueCertFromCa = async ({
|
||||||
caId,
|
caId,
|
||||||
|
certificateTemplateId,
|
||||||
pkiCollectionId,
|
pkiCollectionId,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
commonName,
|
commonName,
|
||||||
@@ -1025,8 +1031,27 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
}: TIssueCertFromCaDTO) => {
|
}: TIssueCertFromCaDTO) => {
|
||||||
const ca = await certificateAuthorityDAL.findById(caId);
|
let ca: TCertificateAuthorities | undefined;
|
||||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
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(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@@ -1047,8 +1072,8 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check PKI collection
|
// check PKI collection
|
||||||
if (pkiCollectionId) {
|
if (collectionId) {
|
||||||
const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId);
|
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
|
||||||
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
|
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
|
||||||
if (pkiCollection.projectId !== ca.projectId) throw new BadRequestError({ message: "Invalid PKI collection" });
|
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)
|
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let altNamesArray: {
|
||||||
|
type: "email" | "dns";
|
||||||
|
value: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
if (altNames) {
|
if (altNames) {
|
||||||
const altNamesArray: {
|
altNamesArray = altNames
|
||||||
type: "email" | "dns";
|
|
||||||
value: string;
|
|
||||||
}[] = altNames
|
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((name) => name.trim())
|
.map((name) => name.trim())
|
||||||
.map((altName) => {
|
.map((altName) => {
|
||||||
@@ -1153,6 +1180,18 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
extensions.push(altNamesExtension);
|
extensions.push(altNamesExtension);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (certificateTemplate) {
|
||||||
|
validateCertificateDetailsAgainstTemplate(
|
||||||
|
{
|
||||||
|
commonName,
|
||||||
|
notBeforeDate,
|
||||||
|
notAfterDate,
|
||||||
|
altNames: altNamesArray.map((entry) => entry.value)
|
||||||
|
},
|
||||||
|
certificateTemplate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const serialNumber = crypto.randomBytes(32).toString("hex");
|
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||||
const leafCert = await x509.X509CertificateGenerator.create({
|
const leafCert = await x509.X509CertificateGenerator.create({
|
||||||
serialNumber,
|
serialNumber,
|
||||||
@@ -1179,8 +1218,9 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
await certificateDAL.transaction(async (tx) => {
|
await certificateDAL.transaction(async (tx) => {
|
||||||
const cert = await certificateDAL.create(
|
const cert = await certificateDAL.create(
|
||||||
{
|
{
|
||||||
caId: ca.id,
|
caId: (ca as TCertificateAuthorities).id,
|
||||||
caCertId: caCert.id,
|
caCertId: caCert.id,
|
||||||
|
certificateTemplateId: certificateTemplate?.id,
|
||||||
status: CertStatus.ACTIVE,
|
status: CertStatus.ACTIVE,
|
||||||
friendlyName: friendlyName || commonName,
|
friendlyName: friendlyName || commonName,
|
||||||
commonName,
|
commonName,
|
||||||
@@ -1200,10 +1240,10 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pkiCollectionId) {
|
if (collectionId) {
|
||||||
await pkiCollectionItemDAL.create(
|
await pkiCollectionItemDAL.create(
|
||||||
{
|
{
|
||||||
pkiCollectionId,
|
pkiCollectionId: collectionId,
|
||||||
certId: cert.id
|
certId: cert.id
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
@@ -1237,6 +1277,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
*/
|
*/
|
||||||
const signCertFromCa = async ({
|
const signCertFromCa = async ({
|
||||||
caId,
|
caId,
|
||||||
|
certificateTemplateId,
|
||||||
csr,
|
csr,
|
||||||
pkiCollectionId,
|
pkiCollectionId,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
@@ -1250,8 +1291,27 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
}: TSignCertFromCaDTO) => {
|
}: TSignCertFromCaDTO) => {
|
||||||
const ca = await certificateAuthorityDAL.findById(caId);
|
let ca: TCertificateAuthorities | undefined;
|
||||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
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(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@@ -1346,11 +1406,12 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let altNamesArray: {
|
||||||
|
type: "email" | "dns";
|
||||||
|
value: string;
|
||||||
|
}[] = [];
|
||||||
if (altNames) {
|
if (altNames) {
|
||||||
const altNamesArray: {
|
altNamesArray = altNames
|
||||||
type: "email" | "dns";
|
|
||||||
value: string;
|
|
||||||
}[] = altNames
|
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((name) => name.trim())
|
.map((name) => name.trim())
|
||||||
.map((altName) => {
|
.map((altName) => {
|
||||||
@@ -1378,6 +1439,18 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
extensions.push(altNamesExtension);
|
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 serialNumber = crypto.randomBytes(32).toString("hex");
|
||||||
const leafCert = await x509.X509CertificateGenerator.create({
|
const leafCert = await x509.X509CertificateGenerator.create({
|
||||||
serialNumber,
|
serialNumber,
|
||||||
@@ -1401,8 +1474,9 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
await certificateDAL.transaction(async (tx) => {
|
await certificateDAL.transaction(async (tx) => {
|
||||||
const cert = await certificateDAL.create(
|
const cert = await certificateDAL.create(
|
||||||
{
|
{
|
||||||
caId: ca.id,
|
caId: (ca as TCertificateAuthorities).id,
|
||||||
caCertId: caCert.id,
|
caCertId: caCert.id,
|
||||||
|
certificateTemplateId: certificateTemplate?.id,
|
||||||
status: CertStatus.ACTIVE,
|
status: CertStatus.ACTIVE,
|
||||||
friendlyName: friendlyName || csrObj.subject,
|
friendlyName: friendlyName || csrObj.subject,
|
||||||
commonName: cn,
|
commonName: cn,
|
||||||
@@ -1422,10 +1496,10 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pkiCollectionId) {
|
if (collectionId) {
|
||||||
await pkiCollectionItemDAL.create(
|
await pkiCollectionItemDAL.create(
|
||||||
{
|
{
|
||||||
pkiCollectionId,
|
pkiCollectionId: collectionId,
|
||||||
certId: cert.id
|
certId: cert.id
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ export type TImportCertToCaDTO = {
|
|||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TIssueCertFromCaDTO = {
|
export type TIssueCertFromCaDTO = {
|
||||||
caId: string;
|
caId?: string;
|
||||||
|
certificateTemplateId?: string;
|
||||||
pkiCollectionId?: string;
|
pkiCollectionId?: string;
|
||||||
friendlyName?: string;
|
friendlyName?: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
@@ -97,8 +98,9 @@ export type TIssueCertFromCaDTO = {
|
|||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TSignCertFromCaDTO = {
|
export type TSignCertFromCaDTO = {
|
||||||
caId: string;
|
caId?: string;
|
||||||
csr: string;
|
csr: string;
|
||||||
|
certificateTemplateId?: string;
|
||||||
pkiCollectionId?: string;
|
pkiCollectionId?: string;
|
||||||
friendlyName?: string;
|
friendlyName?: string;
|
||||||
commonName?: 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 { ActorType } from "../auth/auth-type";
|
||||||
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-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 { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||||
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
||||||
@@ -41,6 +42,7 @@ import {
|
|||||||
TGetProjectKmsKey,
|
TGetProjectKmsKey,
|
||||||
TListProjectAlertsDTO,
|
TListProjectAlertsDTO,
|
||||||
TListProjectCasDTO,
|
TListProjectCasDTO,
|
||||||
|
TListProjectCertificateTemplatesDTO,
|
||||||
TListProjectCertsDTO,
|
TListProjectCertsDTO,
|
||||||
TLoadProjectKmsBackupDTO,
|
TLoadProjectKmsBackupDTO,
|
||||||
TToggleProjectAutoCapitalizationDTO,
|
TToggleProjectAutoCapitalizationDTO,
|
||||||
@@ -73,6 +75,7 @@ type TProjectServiceFactoryDep = {
|
|||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
||||||
|
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
||||||
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
||||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
@@ -112,6 +115,7 @@ export const projectServiceFactory = ({
|
|||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
|
certificateTemplateDAL,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
pkiAlertDAL,
|
pkiAlertDAL,
|
||||||
keyStore,
|
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 ({
|
const updateProjectKmsKey = async ({
|
||||||
projectId,
|
projectId,
|
||||||
kms,
|
kms,
|
||||||
@@ -857,6 +891,7 @@ export const projectServiceFactory = ({
|
|||||||
listProjectCertificates,
|
listProjectCertificates,
|
||||||
listProjectAlerts,
|
listProjectAlerts,
|
||||||
listProjectPkiCollections,
|
listProjectPkiCollections,
|
||||||
|
listProjectCertificateTemplates,
|
||||||
updateVersionLimit,
|
updateVersionLimit,
|
||||||
updateAuditLogsRetention,
|
updateAuditLogsRetention,
|
||||||
updateProjectKmsKey,
|
updateProjectKmsKey,
|
||||||
|
|||||||
@@ -117,3 +117,5 @@ export type TLoadProjectKmsBackupDTO = {
|
|||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetProjectKmsKey = 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"
|
||||||
|
---
|
||||||
@@ -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 |
+13
-1
@@ -108,6 +108,7 @@
|
|||||||
"documentation/platform/pki/overview",
|
"documentation/platform/pki/overview",
|
||||||
"documentation/platform/pki/private-ca",
|
"documentation/platform/pki/private-ca",
|
||||||
"documentation/platform/pki/certificates",
|
"documentation/platform/pki/certificates",
|
||||||
|
"documentation/platform/pki/certificate-templates",
|
||||||
"documentation/platform/pki/alerting"
|
"documentation/platform/pki/alerting"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -701,7 +702,18 @@
|
|||||||
"api-reference/endpoints/certificates/read",
|
"api-reference/endpoints/certificates/read",
|
||||||
"api-reference/endpoints/certificates/revoke",
|
"api-reference/endpoints/certificates/revoke",
|
||||||
"api-reference/endpoints/certificates/delete",
|
"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",
|
Identity = "identity",
|
||||||
CertificateAuthorities = "certificate-authorities",
|
CertificateAuthorities = "certificate-authorities",
|
||||||
Certificates = "certificates",
|
Certificates = "certificates",
|
||||||
|
CertificateTemplates = "certificate-templates",
|
||||||
PkiAlerts = "pki-alerts",
|
PkiAlerts = "pki-alerts",
|
||||||
PkiCollections = "pki-collections",
|
PkiCollections = "pki-collections",
|
||||||
Kms = "kms"
|
Kms = "kms"
|
||||||
@@ -59,6 +60,7 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
| [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.GET_PKI_COLLECTION_ITEMS]: "Get PKI collection items",
|
||||||
[EventType.ADD_PKI_COLLECTION_ITEM]: "Add PKI collection item",
|
[EventType.ADD_PKI_COLLECTION_ITEM]: "Add PKI collection item",
|
||||||
[EventType.DELETE_PKI_COLLECTION_ITEM]: "Delete 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 } = {
|
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||||
|
|||||||
@@ -82,5 +82,9 @@ export enum EventType {
|
|||||||
GET_PKI_COLLECTION_ITEMS = "get-pki-collection-items",
|
GET_PKI_COLLECTION_ITEMS = "get-pki-collection-items",
|
||||||
ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item",
|
ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item",
|
||||||
DELETE_PKI_COLLECTION_ITEM = "delete-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
|
}; // 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 =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@@ -747,7 +787,11 @@ export type Event =
|
|||||||
| GetPkiCollectionItems
|
| GetPkiCollectionItems
|
||||||
| AddPkiCollectionItem
|
| AddPkiCollectionItem
|
||||||
| DeletePkiCollectionItem
|
| DeletePkiCollectionItem
|
||||||
| OrgAdminAccessProjectEvent;
|
| OrgAdminAccessProjectEvent
|
||||||
|
| CreateCertificateTemplate
|
||||||
|
| UpdateCertificateTemplate
|
||||||
|
| GetCertificateTemplate
|
||||||
|
| DeleteCertificateTemplate;
|
||||||
|
|
||||||
export type AuditLog = {
|
export type AuditLog = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -99,9 +99,9 @@ export const useImportCaCertificate = () => {
|
|||||||
export const useCreateCertificate = () => {
|
export const useCreateCertificate = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<TCreateCertificateResponse, {}, TCreateCertificateDTO>({
|
return useMutation<TCreateCertificateResponse, {}, TCreateCertificateDTO>({
|
||||||
mutationFn: async ({ caId, ...body }) => {
|
mutationFn: async (body) => {
|
||||||
const { data } = await apiRequest.post<TCreateCertificateResponse>(
|
const { data } = await apiRequest.post<TCreateCertificateResponse>(
|
||||||
`/api/v1/pki/ca/${caId}/issue-certificate`,
|
"/api/v1/pki/certificates/issue-certificate",
|
||||||
body
|
body
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ export type TImportCaCertificateResponse = {
|
|||||||
|
|
||||||
export type TCreateCertificateDTO = {
|
export type TCreateCertificateDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
caId: string;
|
caId?: string;
|
||||||
|
certificateTemplateId?: string;
|
||||||
pkiCollectionId?: string;
|
pkiCollectionId?: string;
|
||||||
friendlyName?: string;
|
friendlyName?: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { useCreateCertTemplate, useDeleteCertTemplate, useUpdateCertTemplate } from "./mutations";
|
||||||
|
export { useGetCertTemplate } from "./queries";
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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)
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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 = {
|
export type TCertificate = {
|
||||||
id: string;
|
id: string;
|
||||||
caId: string;
|
caId: string;
|
||||||
|
certificateTemplateId?: string;
|
||||||
status: CertStatus;
|
status: CertStatus;
|
||||||
friendlyName: string;
|
friendlyName: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export * from "./auth";
|
|||||||
export * from "./bots";
|
export * from "./bots";
|
||||||
export * from "./ca";
|
export * from "./ca";
|
||||||
export * from "./certificates";
|
export * from "./certificates";
|
||||||
|
export * from "./certificateTemplates";
|
||||||
export * from "./dynamicSecret";
|
export * from "./dynamicSecret";
|
||||||
export * from "./dynamicSecretLease";
|
export * from "./dynamicSecretLease";
|
||||||
export * from "./groups";
|
export * from "./groups";
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export {
|
|||||||
useGetWorkspaceUsers,
|
useGetWorkspaceUsers,
|
||||||
useListWorkspaceCas,
|
useListWorkspaceCas,
|
||||||
useListWorkspaceCertificates,
|
useListWorkspaceCertificates,
|
||||||
|
useListWorkspaceCertificateTemplates,
|
||||||
useListWorkspaceGroups,
|
useListWorkspaceGroups,
|
||||||
useListWorkspacePkiAlerts,
|
useListWorkspacePkiAlerts,
|
||||||
useListWorkspacePkiCollections,
|
useListWorkspacePkiCollections,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { apiRequest } from "@app/config/request";
|
|||||||
import { CaStatus } from "../ca/enums";
|
import { CaStatus } from "../ca/enums";
|
||||||
import { TCertificateAuthority } from "../ca/types";
|
import { TCertificateAuthority } from "../ca/types";
|
||||||
import { TCertificate } from "../certificates/types";
|
import { TCertificate } from "../certificates/types";
|
||||||
|
import { TCertificateTemplate } from "../certificateTemplates/types";
|
||||||
import { TGroupMembership } from "../groups/types";
|
import { TGroupMembership } from "../groups/types";
|
||||||
import { identitiesKeys } from "../identities/queries";
|
import { identitiesKeys } from "../identities/queries";
|
||||||
import { IdentityMembership } from "../identities/types";
|
import { IdentityMembership } from "../identities/types";
|
||||||
@@ -68,7 +69,9 @@ export const workspaceKeys = {
|
|||||||
getWorkspacePkiAlerts: (workspaceId: string) =>
|
getWorkspacePkiAlerts: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "workspace-pki-alerts"] as const,
|
[{ workspaceId }, "workspace-pki-alerts"] as const,
|
||||||
getWorkspacePkiCollections: (workspaceId: string) =>
|
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) => {
|
const fetchWorkspaceById = async (workspaceId: string) => {
|
||||||
@@ -639,3 +642,19 @@ export const useListWorkspacePkiCollections = ({ workspaceId }: { workspaceId: s
|
|||||||
enabled: Boolean(workspaceId)
|
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>
|
<p>{`Cert CN: ${event.metadata.cn}`}</p>
|
||||||
</Td>
|
</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:
|
default:
|
||||||
return <Td />;
|
return <Td />;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -1,6 +1,7 @@
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
import { PkiCollectionSection } from "../PkiAlertsTab/components";
|
import { PkiCollectionSection } from "../PkiAlertsTab/components";
|
||||||
|
import { CertificateTemplatesSection } from "./components/CertificateTemplatesSection";
|
||||||
import { CertificatesSection } from "./components";
|
import { CertificatesSection } from "./components";
|
||||||
|
|
||||||
export const CertificatesTab = () => {
|
export const CertificatesTab = () => {
|
||||||
@@ -13,6 +14,7 @@ export const CertificatesTab = () => {
|
|||||||
exit={{ opacity: 0, translateX: 30 }}
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
>
|
>
|
||||||
<PkiCollectionSection />
|
<PkiCollectionSection />
|
||||||
|
<CertificateTemplatesSection />
|
||||||
<CertificatesSection />
|
<CertificatesSection />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|||||||
+121
-36
@@ -1,5 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
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 { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -7,18 +9,22 @@ import { createNotification } from "@app/components/notifications";
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Select,
|
Select,
|
||||||
SelectItem
|
SelectItem,
|
||||||
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import {
|
import {
|
||||||
CaStatus,
|
CaStatus,
|
||||||
useCreateCertificate,
|
useCreateCertificate,
|
||||||
useGetCert,
|
useGetCert,
|
||||||
|
useGetCertTemplate,
|
||||||
useListWorkspaceCas,
|
useListWorkspaceCas,
|
||||||
|
useListWorkspaceCertificateTemplates,
|
||||||
useListWorkspacePkiCollections
|
useListWorkspacePkiCollections
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||||
@@ -27,6 +33,7 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
|||||||
import { CertificateContent } from "./CertificateContent";
|
import { CertificateContent } from "./CertificateContent";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
|
certificateTemplateId: z.string().optional(),
|
||||||
caId: z.string(),
|
caId: z.string(),
|
||||||
collectionId: z.string().optional(),
|
collectionId: z.string().optional(),
|
||||||
friendlyName: z.string(),
|
friendlyName: z.string(),
|
||||||
@@ -49,6 +56,8 @@ type TCertificateDetails = {
|
|||||||
privateKey: string;
|
privateKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CERT_TEMPLATE_NONE_VALUE = "none";
|
||||||
|
|
||||||
export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
|
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
@@ -65,6 +74,10 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
workspaceId: currentWorkspace?.id || ""
|
workspaceId: currentWorkspace?.id || ""
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: templatesData } = useListWorkspaceCertificateTemplates({
|
||||||
|
workspaceId: currentWorkspace?.id || ""
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync: createCertificate } = useCreateCertificate();
|
const { mutateAsync: createCertificate } = useCreateCertificate();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -72,11 +85,20 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
setValue
|
setValue,
|
||||||
|
watch
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(schema)
|
resolver: zodResolver(schema)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedCertTemplateId = watch("certificateTemplateId");
|
||||||
|
const hasCertTemplateSelected =
|
||||||
|
selectedCertTemplateId !== "" && selectedCertTemplateId !== CERT_TEMPLATE_NONE_VALUE;
|
||||||
|
|
||||||
|
const { data: selectedCertTemplate } = useGetCertTemplate(
|
||||||
|
hasCertTemplateSelected ? (selectedCertTemplateId as string) : ""
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cert) {
|
if (cert) {
|
||||||
reset({
|
reset({
|
||||||
@@ -84,6 +106,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
friendlyName: cert.friendlyName,
|
friendlyName: cert.friendlyName,
|
||||||
commonName: cert.commonName,
|
commonName: cert.commonName,
|
||||||
altNames: cert.altNames,
|
altNames: cert.altNames,
|
||||||
|
certificateTemplateId: cert.certificateTemplateId ?? CERT_TEMPLATE_NONE_VALUE,
|
||||||
ttl: ""
|
ttl: ""
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -92,15 +115,22 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
friendlyName: "",
|
friendlyName: "",
|
||||||
commonName: "",
|
commonName: "",
|
||||||
altNames: "",
|
altNames: "",
|
||||||
ttl: ""
|
ttl: "",
|
||||||
|
certificateTemplateId: CERT_TEMPLATE_NONE_VALUE
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [cert]);
|
}, [cert]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cert && selectedCertTemplate) {
|
||||||
|
setValue("ttl", selectedCertTemplate.ttl);
|
||||||
|
}
|
||||||
|
}, [selectedCertTemplate, cert]);
|
||||||
|
|
||||||
const onFormSubmit = async ({
|
const onFormSubmit = async ({
|
||||||
caId,
|
caId,
|
||||||
collectionId,
|
|
||||||
friendlyName,
|
friendlyName,
|
||||||
|
collectionId,
|
||||||
commonName,
|
commonName,
|
||||||
altNames,
|
altNames,
|
||||||
ttl
|
ttl
|
||||||
@@ -109,8 +139,9 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
if (!currentWorkspace?.slug) return;
|
if (!currentWorkspace?.slug) return;
|
||||||
|
|
||||||
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({
|
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({
|
||||||
|
caId: !selectedCertTemplate ? caId : undefined,
|
||||||
|
certificateTemplateId: selectedCertTemplate ? selectedCertTemplateId : undefined,
|
||||||
projectSlug: currentWorkspace.slug,
|
projectSlug: currentWorkspace.slug,
|
||||||
caId,
|
|
||||||
pkiCollectionId: collectionId,
|
pkiCollectionId: collectionId,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
commonName,
|
commonName,
|
||||||
@@ -160,11 +191,31 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="caId"
|
name="certificateTemplateId"
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<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}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
@@ -177,42 +228,76 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
isDisabled={Boolean(cert)}
|
isDisabled={Boolean(cert)}
|
||||||
>
|
>
|
||||||
{(cas || []).map(({ id, type, dn }) => (
|
<SelectItem value={CERT_TEMPLATE_NONE_VALUE} key="cert-template-none">
|
||||||
<SelectItem value={id} key={`ca-${id}`}>
|
None
|
||||||
{`${caTypeToNameMap[type]}: ${dn}`}
|
</SelectItem>
|
||||||
|
{(templatesData?.certificateTemplates || []).map(({ id, name }) => (
|
||||||
|
<SelectItem value={id} key={`cert-template-${id}`}>
|
||||||
|
{name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{!cert && (
|
{(!selectedCertTemplateId ||
|
||||||
<Controller
|
selectedCertTemplateId === CERT_TEMPLATE_NONE_VALUE ||
|
||||||
control={control}
|
cert) && (
|
||||||
name="collectionId"
|
<>
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
<Controller
|
||||||
<FormControl
|
control={control}
|
||||||
label="Certificate Collection (Optional)"
|
name="caId"
|
||||||
errorText={error?.message}
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
isError={Boolean(error)}
|
<FormControl
|
||||||
className="mt-4"
|
label="Issuing CA"
|
||||||
>
|
errorText={error?.message}
|
||||||
<Select
|
isError={Boolean(error)}
|
||||||
defaultValue={field.value}
|
className="mt-4"
|
||||||
{...field}
|
isRequired
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
isDisabled={Boolean(cert)}
|
|
||||||
>
|
>
|
||||||
{(data?.collections || []).map(({ id, name }) => (
|
<Select
|
||||||
<SelectItem value={id} key={`pki-collection-${id}`}>
|
defaultValue={field.value}
|
||||||
{name}
|
{...field}
|
||||||
</SelectItem>
|
onValueChange={(e) => onChange(e)}
|
||||||
))}
|
className="w-full"
|
||||||
</Select>
|
isDisabled={Boolean(cert)}
|
||||||
</FormControl>
|
>
|
||||||
)}
|
{(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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
+348
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+87
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+117
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
-1
@@ -61,7 +61,7 @@ export const CertificatesSection = () => {
|
|||||||
onClick={() => handlePopUpOpen("certificate")}
|
onClick={() => handlePopUpOpen("certificate")}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Issue Certificate
|
Issue
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
|
|||||||
+1
-1
@@ -209,7 +209,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isLoading && !data?.certificates?.length && (
|
{!isLoading && !data?.certificates?.length && (
|
||||||
<EmptyState title="No certificates have been created" icon={faCertificate} />
|
<EmptyState title="No certificates have been issued" icon={faCertificate} />
|
||||||
)}
|
)}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
+1
@@ -53,6 +53,7 @@ export const formSchema = z.object({
|
|||||||
certificates: generalPermissionSchema,
|
certificates: generalPermissionSchema,
|
||||||
"pki-alerts": generalPermissionSchema,
|
"pki-alerts": generalPermissionSchema,
|
||||||
"pki-collections": generalPermissionSchema,
|
"pki-collections": generalPermissionSchema,
|
||||||
|
"certificate-templates": generalPermissionSchema,
|
||||||
// akhilmhdh: refactor all keys like below
|
// akhilmhdh: refactor all keys like below
|
||||||
[ProjectPermissionSub.SecretApproval]: generalPermissionSchema,
|
[ProjectPermissionSub.SecretApproval]: generalPermissionSchema,
|
||||||
workspace: z
|
workspace: z
|
||||||
|
|||||||
+4
@@ -81,6 +81,10 @@ const SINGLE_PERMISSION_LIST = [
|
|||||||
title: "Certificates",
|
title: "Certificates",
|
||||||
formName: "certificates"
|
formName: "certificates"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Certificate Templates",
|
||||||
|
formName: "certificate-templates"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "PKI Collections",
|
title: "PKI Collections",
|
||||||
formName: "pki-collections"
|
formName: "pki-collections"
|
||||||
|
|||||||
Reference in New Issue
Block a user