Merge pull request #3686 from akhilmhdh/feat/template-k8-issuer

Feat/template k8 issuer
This commit is contained in:
Maidul Islam
2025-05-30 14:16:49 -04:00
committed by GitHub
38 changed files with 5933 additions and 98 deletions

View File

@@ -83,6 +83,7 @@ import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-servi
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service"; import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
import { TPkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service"; import { TPkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
import { TPkiTemplatesServiceFactory } from "@app/services/pki-templates/pki-templates-service";
import { TProjectServiceFactory } from "@app/services/project/project-service"; import { TProjectServiceFactory } from "@app/services/project/project-service";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service"; import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
@@ -271,6 +272,7 @@ declare module "fastify" {
assumePrivileges: TAssumePrivilegeServiceFactory; assumePrivileges: TAssumePrivilegeServiceFactory;
githubOrgSync: TGithubOrgSyncServiceFactory; githubOrgSync: TGithubOrgSyncServiceFactory;
internalCertificateAuthority: TInternalCertificateAuthorityServiceFactory; internalCertificateAuthority: TInternalCertificateAuthorityServiceFactory;
pkiTemplate: TPkiTemplatesServiceFactory;
}; };
// this is exclusive use for middlewares in which we need to inject data // this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer // everywhere else access using service layer

View File

@@ -0,0 +1,24 @@
import slugify from "@sindresorhus/slugify";
import { Knex } from "knex";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasNameCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "name");
if (hasNameCol) {
const templates = await knex(TableName.CertificateTemplate).select("id", "name");
await Promise.all(
templates.map((el) => {
const slugifiedName = el.name
? slugify(`${el.name.slice(0, 16)}-${alphaNumericNanoId(8)}`)
: slugify(alphaNumericNanoId(12));
return knex(TableName.CertificateTemplate).where({ id: el.id }).update({ name: slugifiedName });
})
);
}
}
export async function down(): Promise<void> {}

View File

@@ -10,6 +10,7 @@ import {
ProjectPermissionKmipActions, ProjectPermissionKmipActions,
ProjectPermissionMemberActions, ProjectPermissionMemberActions,
ProjectPermissionPkiSubscriberActions, ProjectPermissionPkiSubscriberActions,
ProjectPermissionPkiTemplateActions,
ProjectPermissionSecretActions, ProjectPermissionSecretActions,
ProjectPermissionSecretRotationActions, ProjectPermissionSecretRotationActions,
ProjectPermissionSecretSyncActions, ProjectPermissionSecretSyncActions,
@@ -35,7 +36,6 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.AuditLogs, ProjectPermissionSub.AuditLogs,
ProjectPermissionSub.IpAllowList, ProjectPermissionSub.IpAllowList,
ProjectPermissionSub.CertificateAuthorities, ProjectPermissionSub.CertificateAuthorities,
ProjectPermissionSub.CertificateTemplates,
ProjectPermissionSub.PkiAlerts, ProjectPermissionSub.PkiAlerts,
ProjectPermissionSub.PkiCollections, ProjectPermissionSub.PkiCollections,
ProjectPermissionSub.SshCertificateAuthorities, ProjectPermissionSub.SshCertificateAuthorities,
@@ -54,6 +54,18 @@ const buildAdminPermissionRules = () => {
); );
}); });
can(
[
ProjectPermissionPkiTemplateActions.Read,
ProjectPermissionPkiTemplateActions.Edit,
ProjectPermissionPkiTemplateActions.Create,
ProjectPermissionPkiTemplateActions.Delete,
ProjectPermissionPkiTemplateActions.IssueCert,
ProjectPermissionPkiTemplateActions.ListCerts
],
ProjectPermissionSub.CertificateTemplates
);
can( can(
[ [
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
@@ -348,7 +360,7 @@ const buildMemberPermissionRules = () => {
ProjectPermissionSub.Certificates ProjectPermissionSub.Certificates
); );
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateTemplates); can([ProjectPermissionPkiTemplateActions.Read], ProjectPermissionSub.CertificateTemplates);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts); can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections); can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
@@ -417,6 +429,7 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities); can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates); can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionPkiTemplateActions.Read, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);

View File

@@ -87,6 +87,15 @@ export enum ProjectPermissionSshHostActions {
IssueHostCert = "issue-host-cert" IssueHostCert = "issue-host-cert"
} }
export enum ProjectPermissionPkiTemplateActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
IssueCert = "issue-cert",
ListCerts = "list-certs"
}
export enum ProjectPermissionPkiSubscriberActions { export enum ProjectPermissionPkiSubscriberActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -200,6 +209,11 @@ export type SshHostSubjectFields = {
hostname: string; hostname: string;
}; };
export type PkiTemplateSubjectFields = {
name: string;
// (dangtony98): consider adding [commonName] as a subject field in the future
};
export type PkiSubscriberSubjectFields = { export type PkiSubscriberSubjectFields = {
name: string; name: string;
// (dangtony98): consider adding [commonName] as a subject field in the future // (dangtony98): consider adding [commonName] as a subject field in the future
@@ -256,7 +270,13 @@ export type ProjectPermissionSet =
] ]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates] | [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] | [
ProjectPermissionPkiTemplateActions,
(
| ProjectPermissionSub.CertificateTemplates
| (ForcedSubject<ProjectPermissionSub.CertificateTemplates> & PkiTemplateSubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
@@ -436,6 +456,21 @@ const PkiSubscriberConditionSchema = z
}) })
.partial(); .partial();
const PkiTemplateConditionSchema = z
.object({
name: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
})
.partial()
])
})
.partial();
const GeneralPermissionSchema = [ const GeneralPermissionSchema = [
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
@@ -527,12 +562,6 @@ const GeneralPermissionSchema = [
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
}),
z.object({ z.object({
subject: z subject: z
.literal(ProjectPermissionSub.SshCertificateAuthorities) .literal(ProjectPermissionSub.SshCertificateAuthorities)
@@ -710,6 +739,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
"When specified, only matching conditions will be allowed to access given resource." "When specified, only matching conditions will be allowed to access given resource."
).optional() ).optional()
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPkiTemplateActions).describe(
"Describe what action an entity can take."
),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
conditions: PkiTemplateConditionSchema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
@@ -720,6 +759,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
"When specified, only matching conditions will be allowed to access given resource." "When specified, only matching conditions will be allowed to access given resource."
).optional() ).optional()
}), }),
...GeneralPermissionSchema ...GeneralPermissionSchema
]); ]);

View File

@@ -211,6 +211,8 @@ import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-co
import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { pkiSubscriberQueueServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-queue"; import { pkiSubscriberQueueServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-queue";
import { pkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service"; import { pkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
import { pkiTemplatesDALFactory } from "@app/services/pki-templates/pki-templates-dal";
import { pkiTemplatesServiceFactory } from "@app/services/pki-templates/pki-templates-service";
import { projectDALFactory } from "@app/services/project/project-dal"; import { projectDALFactory } from "@app/services/project/project-dal";
import { projectQueueFactory } from "@app/services/project/project-queue"; import { projectQueueFactory } from "@app/services/project/project-queue";
import { projectServiceFactory } from "@app/services/project/project-service"; import { projectServiceFactory } from "@app/services/project/project-service";
@@ -847,6 +849,7 @@ export const registerRoutes = async (
const pkiCollectionDAL = pkiCollectionDALFactory(db); const pkiCollectionDAL = pkiCollectionDALFactory(db);
const pkiCollectionItemDAL = pkiCollectionItemDALFactory(db); const pkiCollectionItemDAL = pkiCollectionItemDALFactory(db);
const pkiSubscriberDAL = pkiSubscriberDALFactory(db); const pkiSubscriberDAL = pkiSubscriberDALFactory(db);
const pkiTemplatesDAL = pkiTemplatesDALFactory(db);
const certificateService = certificateServiceFactory({ const certificateService = certificateServiceFactory({
certificateDAL, certificateDAL,
@@ -1754,6 +1757,21 @@ export const registerRoutes = async (
internalCaFns internalCaFns
}); });
const pkiTemplateService = pkiTemplatesServiceFactory({
pkiTemplatesDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
projectDAL,
kmsService,
permissionService,
internalCaFns
});
await secretRotationV2QueueServiceFactory({ await secretRotationV2QueueServiceFactory({
secretRotationV2Service, secretRotationV2Service,
secretRotationV2DAL, secretRotationV2DAL,
@@ -1847,6 +1865,7 @@ export const registerRoutes = async (
pkiAlert: pkiAlertService, pkiAlert: pkiAlertService,
pkiCollection: pkiCollectionService, pkiCollection: pkiCollectionService,
pkiSubscriber: pkiSubscriberService, pkiSubscriber: pkiSubscriberService,
pkiTemplate: pkiTemplateService,
secretScanning: secretScanningService, secretScanning: secretScanningService,
license: licenseService, license: licenseService,
trustedIp: trustedIpService, trustedIp: trustedIpService,

View File

@@ -5,6 +5,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, CERTIFICATE_TEMPLATES } from "@app/lib/api-docs"; import { ApiDocsTags, CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
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 { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
@@ -72,7 +73,7 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
body: z.object({ body: z.object({
caId: z.string().describe(CERTIFICATE_TEMPLATES.CREATE.caId), caId: z.string().describe(CERTIFICATE_TEMPLATES.CREATE.caId),
pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.CREATE.pkiCollectionId), pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.CREATE.pkiCollectionId),
name: z.string().min(1).describe(CERTIFICATE_TEMPLATES.CREATE.name), name: slugSchema().describe(CERTIFICATE_TEMPLATES.CREATE.name),
commonName: validateTemplateRegexField.describe(CERTIFICATE_TEMPLATES.CREATE.commonName), commonName: validateTemplateRegexField.describe(CERTIFICATE_TEMPLATES.CREATE.commonName),
subjectAlternativeName: validateTemplateRegexField.describe( subjectAlternativeName: validateTemplateRegexField.describe(
CERTIFICATE_TEMPLATES.CREATE.subjectAlternativeName CERTIFICATE_TEMPLATES.CREATE.subjectAlternativeName
@@ -141,7 +142,7 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
body: z.object({ body: z.object({
caId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.caId), caId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.caId),
pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.pkiCollectionId), pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.pkiCollectionId),
name: z.string().min(1).optional().describe(CERTIFICATE_TEMPLATES.UPDATE.name), name: slugSchema().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.name),
commonName: validateTemplateRegexField.optional().describe(CERTIFICATE_TEMPLATES.UPDATE.commonName), commonName: validateTemplateRegexField.optional().describe(CERTIFICATE_TEMPLATES.UPDATE.commonName),
subjectAlternativeName: validateTemplateRegexField subjectAlternativeName: validateTemplateRegexField
.optional() .optional()

View File

@@ -5,6 +5,7 @@ import { registerIdentityProjectRouter } from "./identity-project-router";
import { registerMfaRouter } from "./mfa-router"; import { registerMfaRouter } from "./mfa-router";
import { registerOrgRouter } from "./organization-router"; import { registerOrgRouter } from "./organization-router";
import { registerPasswordRouter } from "./password-router"; import { registerPasswordRouter } from "./password-router";
import { registerPkiTemplatesRouter } from "./pki-templates-router";
import { registerProjectMembershipRouter } from "./project-membership-router"; import { registerProjectMembershipRouter } from "./project-membership-router";
import { registerProjectRouter } from "./project-router"; import { registerProjectRouter } from "./project-router";
import { registerServiceTokenRouter } from "./service-token-router"; import { registerServiceTokenRouter } from "./service-token-router";
@@ -15,7 +16,15 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
await server.register(registerUserRouter, { prefix: "/users" }); await server.register(registerUserRouter, { prefix: "/users" });
await server.register(registerServiceTokenRouter, { prefix: "/service-token" }); await server.register(registerServiceTokenRouter, { prefix: "/service-token" });
await server.register(registerPasswordRouter, { prefix: "/password" }); await server.register(registerPasswordRouter, { prefix: "/password" });
await server.register(registerCaRouter, { prefix: "/pki/ca" });
await server.register(
async (pkiRouter) => {
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
await pkiRouter.register(registerPkiTemplatesRouter, { prefix: "/certificate-templates" });
},
{ prefix: "/pki" }
);
await server.register( await server.register(
async (orgRouter) => { async (orgRouter) => {
await orgRouter.register(registerOrgRouter); await orgRouter.register(registerOrgRouter);

View File

@@ -0,0 +1,309 @@
import { z } from "zod";
import { CertificateTemplatesSchema } from "@app/db/schemas";
import { ApiDocsTags } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
import {
validateAltNamesField,
validateCaDateField
} from "@app/services/certificate-authority/certificate-authority-validators";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
export const registerPkiTemplatesRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates],
body: z.object({
name: slugSchema(),
caName: slugSchema({ field: "caName" }),
projectId: z.string(),
commonName: validateTemplateRegexField,
subjectAlternativeName: validateTemplateRegexField,
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"),
keyUsages: z
.nativeEnum(CertKeyUsage)
.array()
.optional()
.default([CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]),
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).array().optional().default([])
}),
response: {
200: z.object({
certificateTemplate: CertificateTemplatesSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateTemplate = await server.services.pkiTemplate.createTemplate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
return { certificateTemplate };
}
});
server.route({
method: "PATCH",
url: "/:templateName",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates],
params: z.object({
templateName: slugSchema()
}),
body: z.object({
name: slugSchema().optional(),
caName: slugSchema(),
projectId: z.string(),
commonName: validateTemplateRegexField.optional(),
subjectAlternativeName: validateTemplateRegexField.optional(),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional(),
keyUsages: z
.nativeEnum(CertKeyUsage)
.array()
.optional()
.default([CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]),
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).array().optional().default([])
}),
response: {
200: z.object({
certificateTemplate: CertificateTemplatesSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateTemplate = await server.services.pkiTemplate.updateTemplate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
templateName: req.params.templateName,
...req.body
});
return { certificateTemplate };
}
});
server.route({
method: "DELETE",
url: "/:templateName",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates],
params: z.object({
templateName: z.string().min(1)
}),
body: z.object({
projectId: z.string()
}),
response: {
200: z.object({
certificateTemplate: CertificateTemplatesSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateTemplate = await server.services.pkiTemplate.deleteTemplate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
templateName: req.params.templateName,
projectId: req.body.projectId
});
return { certificateTemplate };
}
});
server.route({
method: "GET",
url: "/:templateName",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates],
params: z.object({
templateName: slugSchema()
}),
querystring: z.object({
projectId: z.string()
}),
response: {
200: z.object({
certificateTemplate: CertificateTemplatesSchema.extend({
ca: z.object({ id: z.string(), name: z.string() })
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateTemplate = await server.services.pkiTemplate.getTemplateByName({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
templateName: req.params.templateName,
projectId: req.query.projectId
});
return { certificateTemplate };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates],
querystring: z.object({
projectId: z.string(),
limit: z.coerce.number().default(100),
offset: z.coerce.number().default(0)
}),
response: {
200: z.object({
certificateTemplates: CertificateTemplatesSchema.extend({
ca: z.object({ id: z.string(), name: z.string() })
}).array(),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { certificateTemplates, totalCount } = await server.services.pkiTemplate.listTemplate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});
return { certificateTemplates, totalCount };
}
});
server.route({
method: "POST",
url: "/:templateName/issue-certificate",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates],
params: z.object({
templateName: slugSchema()
}),
body: z.object({
projectId: z.string(),
commonName: validateTemplateRegexField,
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"),
keyUsages: z.nativeEnum(CertKeyUsage).array().optional(),
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).array().optional(),
notBefore: validateCaDateField.optional(),
notAfter: validateCaDateField.optional(),
altNames: validateAltNamesField
}),
response: {
200: z.object({
certificate: z.string().trim(),
issuingCaCertificate: z.string().trim(),
certificateChain: z.string().trim(),
privateKey: z.string().trim(),
serialNumber: z.string().trim()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const data = await server.services.pkiTemplate.issueCertificate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
templateName: req.params.templateName,
...req.body
});
return data;
}
});
server.route({
method: "POST",
url: "/:templateName/sign-certificate",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates],
params: z.object({
templateName: slugSchema()
}),
body: z.object({
projectId: z.string(),
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"),
csr: z.string().trim().min(1).max(4096)
}),
response: {
200: z.object({
certificate: z.string().trim(),
issuingCaCertificate: z.string().trim(),
certificateChain: z.string().trim(),
serialNumber: z.string().trim()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const data = await server.services.pkiTemplate.signCertificate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
templateName: req.params.templateName,
...req.body
});
return data;
}
});
};

View File

@@ -1,8 +1,10 @@
/* eslint-disable no-bitwise */
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import { KeyObject } from "crypto"; import { KeyObject } from "crypto";
import RE2 from "re2";
import { z } from "zod"; import { z } from "zod";
import { TPkiSubscribers } from "@app/db/schemas"; import { TCertificateTemplates, TPkiSubscribers } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
@@ -31,6 +33,7 @@ import {
keyAlgorithmToAlgCfg keyAlgorithmToAlgCfg
} from "../certificate-authority-fns"; } from "../certificate-authority-fns";
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal"; import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types";
type TInternalCertificateAuthorityFnsDeps = { type TInternalCertificateAuthorityFnsDeps = {
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa" | "findById">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa" | "findById">;
@@ -257,7 +260,274 @@ export const InternalCertificateAuthorityFns = ({
}; };
}; };
const issueCertificateWithTemplate = async (
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>,
certificateTemplate: TCertificateTemplates,
{ altNames, commonName, ttl, extendedKeyUsages, keyUsages, notAfter, notBefore }: TIssueCertWithTemplateDTO
) => {
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
if (!ca.internalCa?.activeCaCertId)
throw new BadRequestError({ message: "CA does not have a certificate installed" });
const caCert = await certificateAuthorityCertDAL.findById(ca.internalCa.activeCaCertId);
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
const caCertObj = new x509.X509Certificate(decryptedCaCert);
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
let notAfterDate = new Date(new Date().setFullYear(new Date().getFullYear() + 1));
if (notAfter) {
notAfterDate = new Date(notAfter);
} else if (ttl) {
notAfterDate = new Date(new Date().getTime() + ms(ttl));
}
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter);
// check not before constraint
if (notBeforeDate < caCertNotBeforeDate) {
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
}
// check not after constraint
if (notAfterDate > caCertNotAfterDate) {
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
}
const commonNameRegex = new RE2(certificateTemplate.commonName);
if (!commonNameRegex.test(commonName)) {
throw new BadRequestError({
message: "Invalid common name based on template policy"
});
}
if (notAfterDate.getTime() - notBeforeDate.getTime() > ms(certificateTemplate.ttl)) {
throw new BadRequestError({
message: "Invalid validity date based on template policy"
});
}
const subjectAlternativeNameRegex = new RE2(certificateTemplate.subjectAlternativeName);
altNames.split(",").forEach((altName) => {
if (!subjectAlternativeNameRegex.test(altName)) {
throw new BadRequestError({
message: "Invalid subject alternative name based on template policy"
});
}
});
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
name: `CN=${commonName}`,
keys: leafKeys,
signingAlgorithm: alg,
extensions: [
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
],
attributes: [new x509.ChallengePasswordAttribute("password")]
});
const { caPrivateKey, caSecret } = await getCaCredentials({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService
});
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const appCfg = getConfig();
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false),
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
new x509.AuthorityInfoAccessExtension({
caIssuers: new x509.GeneralName("url", caIssuerUrl)
}),
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
];
let selectedKeyUsages: CertKeyUsage[] = keyUsages ?? [];
if (keyUsages === undefined && !certificateTemplate) {
selectedKeyUsages = [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT];
}
if (keyUsages === undefined && certificateTemplate) {
selectedKeyUsages = (certificateTemplate.keyUsages ?? []) as CertKeyUsage[];
}
if (keyUsages?.length && certificateTemplate) {
const validKeyUsages = certificateTemplate.keyUsages || [];
if (keyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) {
throw new BadRequestError({
message: "Invalid key usage value based on template policy"
});
}
selectedKeyUsages = keyUsages;
}
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
if (keyUsagesBitValue) {
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
}
// handle extended key usages
let selectedExtendedKeyUsages: CertExtendedKeyUsage[] = extendedKeyUsages ?? [];
if (extendedKeyUsages === undefined && certificateTemplate) {
selectedExtendedKeyUsages = (certificateTemplate.extendedKeyUsages ?? []) as CertExtendedKeyUsage[];
}
if (extendedKeyUsages?.length && certificateTemplate) {
const validExtendedKeyUsages = certificateTemplate.extendedKeyUsages || [];
if (extendedKeyUsages.some((eku) => !validExtendedKeyUsages.includes(eku))) {
throw new BadRequestError({
message: "Invalid extended key usage value based on template policy"
});
}
selectedExtendedKeyUsages = extendedKeyUsages;
}
if (selectedExtendedKeyUsages.length) {
extensions.push(
new x509.ExtendedKeyUsageExtension(
selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]),
true
)
);
}
let altNamesArray: { type: "email" | "dns"; value: string }[] = [];
if (altNames) {
altNamesArray = altNames.split(",").map((altName) => {
if (z.string().email().safeParse(altName).success) {
return { type: "email", value: altName };
}
if (isFQDN(altName, { allow_wildcard: true })) {
return { type: "dns", value: altName };
}
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
}
const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,
issuer: caCertObj.subject,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingKey: caPrivateKey,
publicKey: csrObj.publicKey,
signingAlgorithm: alg,
extensions
});
const skLeafObj = KeyObject.from(leafKeys.privateKey);
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
});
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: Buffer.from(skLeaf)
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caCertId: caCert.id,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChainPem)
});
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
caCertId: caCert.id,
status: CertStatus.ACTIVE,
friendlyName: commonName,
commonName,
altNames,
serialNumber,
notBefore: notBeforeDate,
notAfter: notAfterDate,
keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages,
projectId: ca.projectId,
certificateTemplateId: certificateTemplate.id
},
tx
);
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate,
encryptedCertificateChain
},
tx
);
await certificateSecretDAL.create(
{
certId: cert.id,
encryptedPrivateKey
},
tx
);
});
return {
certificate: leafCert.toString("pem"),
certificateChain: certificateChainPem,
issuingCaCertificate,
privateKey: skLeaf,
serialNumber,
ca,
template: certificateTemplate
};
};
return { return {
issueCertificate issueCertificate,
issueCertificateWithTemplate
}; };
}; };

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-bitwise */ /* eslint-disable no-bitwise */
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import crypto, { KeyObject } from "crypto"; import crypto, { KeyObject } from "crypto";
@@ -16,6 +16,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
@@ -1952,15 +1953,15 @@ export const internalCertificateAuthorityServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager actionProjectType: ActionProjectType.CertificateManager
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.CertificateTemplates
);
const certificateTemplates = await certificateTemplateDAL.find({ caId }); const certificateTemplates = await certificateTemplateDAL.find({ caId });
return { return {
certificateTemplates, certificateTemplates: certificateTemplates.filter((el) =>
permission.can(
ProjectPermissionPkiTemplateActions.Read,
subject(ProjectPermissionSub.CertificateTemplates, { name: el.name })
)
),
ca: expandInternalCa(ca) ca: expandInternalCa(ca)
}; };
}; };

View File

@@ -221,3 +221,13 @@ export type TOrderCertificateForSubscriberDTO = {
subscriberId: string; subscriberId: string;
caType: CaType; caType: CaType;
}; };
export type TIssueCertWithTemplateDTO = {
commonName: string;
altNames: string;
ttl: string;
notBefore?: string;
notAfter?: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
};

View File

@@ -18,3 +18,20 @@ export const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({
caName: z.string() caName: z.string()
}) })
); );
export const sanitizedCertificateTemplateV2 = CertificateTemplatesSchema.pick({
id: true,
caId: true,
name: true,
commonName: true,
subjectAlternativeName: true,
pkiCollectionId: true,
ttl: true,
keyUsages: true,
extendedKeyUsages: true
}).merge(
z.object({
projectId: z.string(),
caName: z.string()
})
);

View File

@@ -1,11 +1,14 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { ActionProjectType, TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas"; import { ActionProjectType, TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
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 {
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -78,8 +81,8 @@ export const certificateTemplateServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionPkiTemplateActions.Create,
ProjectPermissionSub.CertificateTemplates subject(ProjectPermissionSub.CertificateTemplates, { name })
); );
return certificateTemplateDAL.transaction(async (tx) => { return certificateTemplateDAL.transaction(async (tx) => {
@@ -140,8 +143,8 @@ export const certificateTemplateServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionPkiTemplateActions.Edit,
ProjectPermissionSub.CertificateTemplates subject(ProjectPermissionSub.CertificateTemplates, { name: certTemplate.name })
); );
if (caId) { if (caId) {
@@ -153,6 +156,13 @@ export const certificateTemplateServiceFactory = ({
} }
} }
if (name) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Create,
subject(ProjectPermissionSub.CertificateTemplates, { name })
);
}
return certificateTemplateDAL.transaction(async (tx) => { return certificateTemplateDAL.transaction(async (tx) => {
await certificateTemplateDAL.updateById( await certificateTemplateDAL.updateById(
certTemplate.id, certTemplate.id,
@@ -198,8 +208,8 @@ export const certificateTemplateServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionPkiTemplateActions.Delete,
ProjectPermissionSub.CertificateTemplates subject(ProjectPermissionSub.CertificateTemplates, { name: certTemplate.name })
); );
await certificateTemplateDAL.deleteById(certTemplate.id); await certificateTemplateDAL.deleteById(certTemplate.id);
@@ -225,8 +235,8 @@ export const certificateTemplateServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionPkiTemplateActions.Read,
ProjectPermissionSub.CertificateTemplates subject(ProjectPermissionSub.CertificateTemplates, { name: certTemplate.name })
); );
return certTemplate; return certTemplate;
@@ -267,8 +277,8 @@ export const certificateTemplateServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionPkiTemplateActions.Edit,
ProjectPermissionSub.CertificateTemplates subject(ProjectPermissionSub.CertificateTemplates, { name: certTemplate.name })
); );
const appCfg = getConfig(); const appCfg = getConfig();
@@ -350,8 +360,8 @@ export const certificateTemplateServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionPkiTemplateActions.Edit,
ProjectPermissionSub.CertificateTemplates subject(ProjectPermissionSub.CertificateTemplates, { name: certTemplate.name })
); );
const originalCaEstConfig = await certificateTemplateEstConfigDAL.findOne({ const originalCaEstConfig = await certificateTemplateEstConfigDAL.findOne({
@@ -430,8 +440,8 @@ export const certificateTemplateServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionPkiTemplateActions.Edit,
ProjectPermissionSub.CertificateTemplates subject(ProjectPermissionSub.CertificateTemplates, { name: certTemplate.name })
); );
} }

View File

@@ -0,0 +1,102 @@
import { Knex } from "knex";
import { Tables } from "knex/types/tables";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt, TFindReturn } from "@app/lib/knex";
export type TPkiTemplatesDALFactory = ReturnType<typeof pkiTemplatesDALFactory>;
export const pkiTemplatesDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.CertificateTemplate);
const findOne = async (
filter: Partial<Tables[TableName.CertificateTemplate]["base"] & { projectId: string }>,
tx?: Knex
) => {
try {
const { projectId, ...templateFilters } = filter;
const res = await (tx || db.replicaNode())(TableName.CertificateTemplate)
.join(
TableName.CertificateAuthority,
`${TableName.CertificateAuthority}.id`,
`${TableName.CertificateTemplate}.caId`
)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter(templateFilters, TableName.CertificateTemplate))
.where((qb) => {
if (projectId) {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
void qb.where(buildFindFilter({ projectId }, TableName.CertificateAuthority));
}
})
.select(selectAllTableCols(TableName.CertificateTemplate))
.select(db.ref("name").withSchema(TableName.CertificateAuthority).as("caName"))
.select(db.ref("projectId").withSchema(TableName.CertificateAuthority))
.first();
if (!res) return undefined;
return { ...res, ca: { id: res.caId, name: res.caName } };
} catch (error) {
throw new DatabaseError({ error, name: "Find one" });
}
};
const find = async <
TCount extends boolean = false,
TCountDistinct extends keyof Tables[TableName.CertificateTemplate]["base"] | undefined = undefined
>(
filter: TFindFilter<Tables[TableName.CertificateTemplate]["base"]> & { projectId: string },
{
offset,
limit,
sort,
count,
tx,
countDistinct
}: TFindOpt<Tables[TableName.CertificateTemplate]["base"], TCount, TCountDistinct> = {}
) => {
try {
const { projectId, ...templateFilters } = filter;
const query = (tx || db.replicaNode())(TableName.CertificateTemplate)
.join(
TableName.CertificateAuthority,
`${TableName.CertificateAuthority}.id`,
`${TableName.CertificateTemplate}.caId`
)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter(templateFilters, TableName.CertificateTemplate))
.where((qb) => {
if (projectId) {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
void qb.where(buildFindFilter({ projectId }, TableName.CertificateAuthority));
}
})
.select(selectAllTableCols(TableName.CertificateTemplate))
.select(db.ref("projectId").withSchema(TableName.CertificateAuthority))
.select(db.ref("name").withSchema(TableName.CertificateAuthority).as("caName"));
if (countDistinct) {
void query.countDistinct(countDistinct);
} else if (count) {
void query.select(db.raw("COUNT(*) OVER() AS count"));
}
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const res = (await query) as TFindReturn<typeof query, TCountDistinct extends undefined ? TCount : true>;
return res.map((el) => ({ ...el, ca: { id: el.caId, name: el.caName } }));
} catch (error) {
throw new DatabaseError({ error, name: "Find one" });
}
};
return { ...orm, find, findOne };
};

View File

@@ -0,0 +1,644 @@
/* eslint-disable no-bitwise */
import { ForbiddenError, subject } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import RE2 from "re2";
import { ActionProjectType } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal";
import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
import {
CertExtendedKeyUsage,
CertExtendedKeyUsageOIDToName,
CertKeyAlgorithm,
CertKeyUsage,
CertStatus
} from "../certificate/certificate-types";
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { CaStatus } from "../certificate-authority/certificate-authority-enums";
import {
createSerialNumber,
expandInternalCa,
getCaCertChain,
getCaCredentials,
keyAlgorithmToAlgCfg,
parseDistinguishedName
} from "../certificate-authority/certificate-authority-fns";
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority/certificate-authority-secret-dal";
import { InternalCertificateAuthorityFns } from "../certificate-authority/internal/internal-certificate-authority-fns";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
import { TPkiTemplatesDALFactory } from "./pki-templates-dal";
import {
TCreatePkiTemplateDTO,
TDeletePkiTemplateDTO,
TGetPkiTemplateDTO,
TIssueCertPkiTemplateDTO,
TListPkiTemplateDTO,
TSignCertPkiTemplateDTO,
TUpdatePkiTemplateDTO
} from "./pki-templates-types";
type TPkiTemplatesServiceFactoryDep = {
pkiTemplatesDAL: TPkiTemplatesDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
certificateAuthorityDAL: Pick<
TCertificateAuthorityDALFactory,
| "findByIdWithAssociatedCa"
| "findById"
| "transaction"
| "create"
| "updateById"
| "findWithAssociatedCa"
| "findOne"
>;
internalCaFns: ReturnType<typeof InternalCertificateAuthorityFns>;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
certificateDAL: Pick<
TCertificateDALFactory,
"create" | "transaction" | "countCertificatesForPkiSubscriber" | "findLatestActiveCertForSubscriber" | "find"
>;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create" | "findOne">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create" | "findOne">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById" | "find">;
};
export type TPkiTemplatesServiceFactory = ReturnType<typeof pkiTemplatesServiceFactory>;
export const pkiTemplatesServiceFactory = ({
pkiTemplatesDAL,
permissionService,
internalCaFns,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateDAL,
certificateBodyDAL,
kmsService,
projectDAL
}: TPkiTemplatesServiceFactoryDep) => {
const createTemplate = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
caName,
commonName,
extendedKeyUsages,
keyUsages,
name,
subjectAlternativeName,
ttl,
projectId
}: TCreatePkiTemplateDTO) => {
const ca = await certificateAuthorityDAL.findOne({ name: caName, projectId });
if (!ca) {
throw new NotFoundError({
message: `CA with name ${caName} not found`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: ca.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Create,
subject(ProjectPermissionSub.CertificateTemplates, { name })
);
const existingTemplate = await pkiTemplatesDAL.findOne({ name, projectId: ca.projectId });
if (existingTemplate) {
throw new BadRequestError({ message: `Template with name ${name} already exists.` });
}
const newTemplate = await pkiTemplatesDAL.create({
caId: ca.id,
name,
commonName,
subjectAlternativeName,
ttl,
keyUsages,
extendedKeyUsages
});
return newTemplate;
};
const updateTemplate = async ({
templateName,
actor,
actorId,
actorAuthMethod,
actorOrgId,
caName,
commonName,
extendedKeyUsages,
keyUsages,
name,
subjectAlternativeName,
ttl,
projectId
}: TUpdatePkiTemplateDTO) => {
const certTemplate = await pkiTemplatesDAL.findOne({ name: templateName, projectId });
if (!certTemplate) {
throw new NotFoundError({
message: `Certificate template with name ${templateName} not found`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: certTemplate.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Edit,
subject(ProjectPermissionSub.CertificateTemplates, { name: templateName })
);
let caId;
if (caName) {
const ca = await certificateAuthorityDAL.findOne({ name: caName, projectId });
if (!ca || ca.projectId !== certTemplate.projectId) {
throw new NotFoundError({
message: `CA with name ${caName} not found`
});
}
caId = ca.id;
}
if (name) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Edit,
subject(ProjectPermissionSub.CertificateTemplates, { name })
);
const existingTemplate = await pkiTemplatesDAL.findOne({ name, projectId });
if (existingTemplate && existingTemplate.id !== certTemplate.id) {
throw new BadRequestError({ message: `Template with name ${name} already exists.` });
}
}
const updatedTemplate = await pkiTemplatesDAL.updateById(certTemplate.id, {
caId,
name,
commonName,
subjectAlternativeName,
ttl,
keyUsages,
extendedKeyUsages
});
return updatedTemplate;
};
const deleteTemplate = async ({
templateName,
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId
}: TDeletePkiTemplateDTO) => {
const certTemplate = await pkiTemplatesDAL.findOne({ name: templateName, projectId });
if (!certTemplate) {
throw new NotFoundError({
message: `Certificate template with name ${templateName} not found`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: certTemplate.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Delete,
subject(ProjectPermissionSub.CertificateTemplates, { name: templateName })
);
const deletedTemplate = await pkiTemplatesDAL.deleteById(certTemplate.id);
return deletedTemplate;
};
const getTemplateByName = async ({
templateName,
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId
}: TGetPkiTemplateDTO) => {
const certTemplate = await pkiTemplatesDAL.findOne({ name: templateName, projectId });
if (!certTemplate) {
throw new NotFoundError({
message: `Certificate template with name ${templateName} not found`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: certTemplate.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Read,
subject(ProjectPermissionSub.CertificateTemplates, { name: templateName })
);
return certTemplate;
};
const listTemplate = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId,
limit,
offset
}: TListPkiTemplateDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
const certTemplate = await pkiTemplatesDAL.find({ projectId }, { limit, offset, count: true });
return {
certificateTemplates: certTemplate.filter((el) =>
permission.can(
ProjectPermissionPkiTemplateActions.Read,
subject(ProjectPermissionSub.CertificateTemplates, { name: el.name })
)
),
totalCount: Number(certTemplate?.[0]?.count ?? 0)
};
};
const issueCertificate = async ({
templateName,
projectId,
commonName,
altNames,
ttl,
notBefore,
notAfter,
actorId,
actorAuthMethod,
actor,
actorOrgId,
keyUsages,
extendedKeyUsages
}: TIssueCertPkiTemplateDTO) => {
const certTemplate = await pkiTemplatesDAL.findOne({ name: templateName, projectId });
if (!certTemplate) {
throw new NotFoundError({
message: `Certificate template with name ${templateName} not found`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: certTemplate.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.IssueCert,
subject(ProjectPermissionSub.CertificateTemplates, { name: templateName })
);
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certTemplate.caId);
if (ca.internalCa?.id) {
return internalCaFns.issueCertificateWithTemplate(ca, certTemplate, {
altNames,
commonName,
ttl,
extendedKeyUsages,
keyUsages,
notAfter,
notBefore
});
}
throw new BadRequestError({ message: "CA does not support immediate issuance of certificates" });
};
const signCertificate = async ({
templateName,
csr,
projectId,
actorId,
actorAuthMethod,
actor,
actorOrgId,
ttl
}: TSignCertPkiTemplateDTO) => {
const certTemplate = await pkiTemplatesDAL.findOne({ name: templateName, projectId });
if (!certTemplate) {
throw new NotFoundError({
message: `Certificate template with name ${templateName} not found`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: certTemplate.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.IssueCert,
subject(ProjectPermissionSub.CertificateTemplates, { name: templateName })
);
const appCfg = getConfig();
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certTemplate.caId);
if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${certTemplate.caId}' not found` });
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
if (!ca.internalCa?.activeCaCertId)
throw new BadRequestError({ message: "CA does not have a certificate installed" });
const caCert = await certificateAuthorityCertDAL.findById(ca.internalCa.activeCaCertId);
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
const caCertObj = new x509.X509Certificate(decryptedCaCert);
const notBeforeDate = new Date();
const notAfterDate = new Date(new Date().getTime() + ms(ttl ?? "0"));
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter);
// check not before constraint
if (notBeforeDate < caCertNotBeforeDate) {
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
}
// check not after constraint
if (notAfterDate > caCertNotAfterDate) {
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
}
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
const csrObj = new x509.Pkcs10CertificateRequest(csr);
const dn = parseDistinguishedName(csrObj.subject);
const cn = dn.commonName;
if (!cn)
throw new BadRequestError({
message: "Missing common name on CSR"
});
const commonNameRegex = new RE2(certTemplate.commonName);
if (!commonNameRegex.test(cn)) {
throw new BadRequestError({
message: "Invalid common name based on template policy"
});
}
if (ms(ttl) > ms(certTemplate.ttl)) {
throw new BadRequestError({
message: "Invalid validity date based on template policy"
});
}
const { caPrivateKey, caSecret } = await getCaCredentials({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService
});
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
new x509.AuthorityInfoAccessExtension({
caIssuers: new x509.GeneralName("url", caIssuerUrl)
}),
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
];
// handle key usages
const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension | undefined; // Better to type as optional
let selectedKeyUsages: CertKeyUsage[] = [];
if (csrKeyUsageExtension && csrKeyUsageExtension.usages) {
selectedKeyUsages = Object.values(CertKeyUsage).filter(
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0
);
const validKeyUsages = certTemplate.keyUsages || [];
if (selectedKeyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) {
throw new BadRequestError({
message: "Invalid key usage value based on template policy"
});
}
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
if (keyUsagesBitValue) {
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
}
}
// handle extended key usage
const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension | undefined;
let selectedExtendedKeyUsages: CertExtendedKeyUsage[] = [];
if (csrExtendedKeyUsageExtension && csrExtendedKeyUsageExtension.usages.length > 0) {
selectedExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map(
(ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]
);
if (selectedExtendedKeyUsages.some((eku) => !certTemplate?.extendedKeyUsages?.includes(eku))) {
throw new BadRequestError({
message: "Invalid extended key usage value based on subscriber's specified extended key usages"
});
}
if (selectedExtendedKeyUsages.length) {
extensions.push(
new x509.ExtendedKeyUsageExtension(
selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]),
true
)
);
}
}
// attempt to read from CSR if altNames is not explicitly provided
let altNamesArray: {
type: "email" | "dns";
value: string;
}[] = [];
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
if (sanExtension) {
const sanNames = new x509.GeneralNames(sanExtension.value);
altNamesArray = sanNames.items
.filter((value) => value.type === "email" || value.type === "dns")
.map((name) => ({
type: name.type as "email" | "dns",
value: name.value
}));
}
if (altNamesArray.length) {
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
}
const subjectAlternativeNameRegex = new RE2(certTemplate.subjectAlternativeName);
altNamesArray.forEach((altName) => {
if (!subjectAlternativeNameRegex.test(altName.value)) {
throw new BadRequestError({
message: "Invalid subject alternative name based on template policy"
});
}
});
const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,
issuer: caCertObj.subject,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingKey: caPrivateKey,
publicKey: csrObj.publicKey,
signingAlgorithm: alg,
extensions
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caCertId: ca.internalCa.activeCaCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChainPem)
});
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
caCertId: caCert.id,
status: CertStatus.ACTIVE,
friendlyName: cn,
commonName: cn,
altNames: altNamesArray.map((el) => el.value).join(","),
serialNumber,
notBefore: notBeforeDate,
notAfter: notAfterDate,
keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages,
projectId
},
tx
);
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate,
encryptedCertificateChain
},
tx
);
return cert;
});
return {
certificate: leafCert.toString("pem"),
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
issuingCaCertificate,
serialNumber,
ca: expandInternalCa(ca),
commonName: cn,
template: certTemplate
};
};
return {
createTemplate,
updateTemplate,
getTemplateByName,
listTemplate,
deleteTemplate,
signCertificate,
issueCertificate
};
};

View File

@@ -0,0 +1,53 @@
import { TProjectPermission } from "@app/lib/types";
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
export type TCreatePkiTemplateDTO = {
caName: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
} & TProjectPermission;
export type TUpdatePkiTemplateDTO = {
templateName: string;
caName?: string;
name?: string;
commonName?: string;
subjectAlternativeName?: string;
ttl?: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
} & TProjectPermission;
export type TListPkiTemplateDTO = {
limit?: number;
offset?: number;
} & TProjectPermission;
export type TGetPkiTemplateDTO = {
templateName: string;
} & TProjectPermission;
export type TDeletePkiTemplateDTO = {
templateName: string;
} & TProjectPermission;
export type TIssueCertPkiTemplateDTO = {
templateName: string;
commonName: string;
altNames: string;
ttl: string;
notBefore?: string;
notAfter?: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
} & TProjectPermission;
export type TSignCertPkiTemplateDTO = {
templateName: string;
csr: string;
ttl: string;
} & TProjectPermission;

View File

@@ -17,6 +17,7 @@ import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionPkiSubscriberActions, ProjectPermissionPkiSubscriberActions,
ProjectPermissionPkiTemplateActions,
ProjectPermissionSecretActions, ProjectPermissionSecretActions,
ProjectPermissionSshHostActions, ProjectPermissionSshHostActions,
ProjectPermissionSub ProjectPermissionSub
@@ -1131,15 +1132,15 @@ export const projectServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager actionProjectType: ActionProjectType.CertificateManager
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.CertificateTemplates
);
const certificateTemplates = await certificateTemplateDAL.getCertTemplatesByProjectId(projectId); const certificateTemplates = await certificateTemplateDAL.getCertTemplatesByProjectId(projectId);
return { return {
certificateTemplates certificateTemplates: certificateTemplates.filter((el) =>
permission.can(
ProjectPermissionPkiTemplateActions.Read,
subject(ProjectPermissionSub.CertificateTemplates, { name: el.name })
)
)
}; };
}; };

View File

@@ -21,20 +21,21 @@ A typical workflow for using the Infisical PKI Issuer to issue certificates for
3. Installing `cert-manager` into your Kubernetes cluster. 3. Installing `cert-manager` into your Kubernetes cluster.
4. Installing the Infisical PKI Issuer controller into your Kubernetes cluster. 4. Installing the Infisical PKI Issuer controller into your Kubernetes cluster.
5. Creating an `Issuer` or `ClusterIssuer` resource in your Kubernetes cluster to represent the Infisical PKI issuer you wish to use. 5. Creating an `Issuer` or `ClusterIssuer` resource in your Kubernetes cluster to represent the Infisical PKI issuer you wish to use.
6. Creating a `Certificate` resource in your Kubernetes cluster to represent a certificate you wish to issue. As part of this step, you specify the Kubernetes `Secret` to create and store the issued certificate and private key. 6. Create the approver policy to accept certificate request.
7. Consuming the issued certificate across your Kubernetes resources from the specified Kubernetes `Secret`. 7. Creating a `Certificate` resource in your Kubernetes cluster to represent a certificate you wish to issue. As part of this step, you specify the Kubernetes `Secret` to create and store the issued certificate and private key.
8. Consuming the issued certificate across your Kubernetes resources from the specified Kubernetes `Secret`.
## Guide ## Guide
In the following steps, we explore how to install the Infisical PKI Issuer using [kubectl](https://github.com/kubernetes/kubectl) and use it to obtain certificates for your Kubernetes resources. In the following steps, we explore how to install the Infisical PKI Issuer using [kubectl](https://github.com/kubernetes/kubectl) and use it to obtain certificates for your Kubernetes resources.
<Steps> <Steps>
<Step title="Create an identity in Infisical"> <Step title="Create an identity in Infisical">
Follow the instructions [here](/documentation/platform/identities/universal-auth) to configure a [machine identity](/documentation/platform/identities/machine-identities) in Infisical with Universal Auth. Follow the instructions [here](/documentation/platform/identities/universal-auth) to configure a [machine identity](/documentation/platform/identities/machine-identities) in Infisical with Universal Auth.
By the end of this step, you should have a **Client ID** and **Client Secret** on hand as part of the Universal Auth configuration for the Infisical PKI Issuer to authenticate with Infisical; this will be useful in steps 4 and 5. By the end of this step, you should have a **Client ID** and **Client Secret** on hand as part of the Universal Auth configuration for the Infisical PKI Issuer to authenticate with Infisical; this will be useful in steps 4 and 5.
<Note> <Note>
Currently, the Infisical PKI Issuer only supports authenticating with Infisical via the [Universal Auth](/documentation/platform/identities/universal-auth) authentication method. Currently, the Infisical PKI Issuer only supports authenticating with Infisical via the [Universal Auth](/documentation/platform/identities/universal-auth) authentication method.
@@ -43,14 +44,14 @@ In the following steps, we explore how to install the Infisical PKI Issuer using
</Step> </Step>
<Step title="Install cert-manager"> <Step title="Install cert-manager">
Install `cert-manager` into your Kubernetes cluster by following the instructions [here](https://cert-manager.io/docs/installation/) or by running the following command: Install `cert-manager` into your Kubernetes cluster by following the instructions [here](https://cert-manager.io/docs/installation/) or by running the following command:
```bash ```bash
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.3/cert-manager.yaml kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.3/cert-manager.yaml
``` ```
</Step> </Step>
<Step title="Install the Issuer Controller"> <Step title="Install the Issuer Controller">
Install the Infisical PKI Issuer controller into your Kubernetes cluster by running the following command: Install the Infisical PKI Issuer controller into your Kubernetes cluster by running the following command:
```bash ```bash
kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical-issuer/main/build/install.yaml kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical-issuer/main/build/install.yaml
``` ```
@@ -76,7 +77,7 @@ In the following steps, we explore how to install the Infisical PKI Issuer using
data: data:
clientSecret: <client_secret> clientSecret: <client_secret>
``` ```
```bash ```bash
kubectl apply -f secret-issuer.yaml kubectl apply -f secret-issuer.yaml
``` ```
@@ -84,7 +85,7 @@ In the following steps, we explore how to install the Infisical PKI Issuer using
</Tabs> </Tabs>
</Step> </Step>
<Step title="Create Infisical PKI Issuer"> <Step title="Create Infisical PKI Issuer">
Next, create the Infisical PKI Issuer by filling out `url`, `clientId`, either `caId` or `certificateTemplateId`, and applying the following configuration file for the `Issuer` resource. Next, create the Infisical PKI Issuer by filling out `url`, `clientId`, `projectId` or `certificateTemplateName`, and applying the following configuration file for the `Issuer` resource.
This configuration file specifies the connection details to your Infisical PKI CA to be used for issuing certificates. This configuration file specifies the connection details to your Infisical PKI CA to be used for issuing certificates.
```yaml infisical-issuer.yaml ```yaml infisical-issuer.yaml
@@ -95,8 +96,8 @@ In the following steps, we explore how to install the Infisical PKI Issuer using
namespace: <namespace_you_want_to_issue_certificates_in> namespace: <namespace_you_want_to_issue_certificates_in>
spec: spec:
url: "https://app.infisical.com" # the URL of your Infisical instance url: "https://app.infisical.com" # the URL of your Infisical instance
caId: <ca_id> # the ID of the CA you want to use to issue certificates projectId: <project_id> # the ID of the project you want to use to issue certificates
certificateTemplateId: <certificate_template_id> # the ID of the certificate template you want to use to issue certificates against certificateTemplateName: <certificate_template_name> # the name of the certificate template you want to use to issue certificates against
authentication: authentication:
universalAuth: universalAuth:
clientId: <client_id> # the Client ID from step 1 clientId: <client_id> # the Client ID from step 1
@@ -104,20 +105,11 @@ In the following steps, we explore how to install the Infisical PKI Issuer using
name: "issuer-infisical-client-secret" name: "issuer-infisical-client-secret"
key: "clientSecret" key: "clientSecret"
``` ```
``` ```
kubectl apply -f infisical-issuer.yaml kubectl apply -f infisical-issuer.yaml
``` ```
<Warning>
The Infisical PKI Issuer supports issuing certificates against a specific CA or a specific certificate template.
For this reason, you should only fill in the `caId` or the `certificateTemplateId` field but not both.
We recommend using the `certificateTemplateId` field to issue certificates against a specific [certificate template](/documentation/platform/pki/certificate-templates)
since templates let you enforce constraints on issued certificates and may have alerting policies bound to them.
</Warning>
You can check that the issuer was created successfully by running the following command: You can check that the issuer was created successfully by running the following command:
```bash ```bash
@@ -128,16 +120,60 @@ In the following steps, we explore how to install the Infisical PKI Issuer using
NAME AGE NAME AGE
issuer-infisical 21h issuer-infisical 21h
``` ```
<Note> <Note>
An `Issuer` is a namespaced resource, and it is not possible to issue certificates from an `Issuer` in a different namespace. An `Issuer` is a namespaced resource, and it is not possible to issue certificates from an `Issuer` in a different namespace.
This means you will need to create an `Issuer` in each namespace you wish to obtain `Certificates` in. This means you will need to create an `Issuer` in each namespace you wish to obtain `Certificates` in.
If you want to create a single `Issuer` that can be consumed in multiple namespaces, you should consider creating a `ClusterIssuer` resource. This is almost identical to the `Issuer` resource, however is non-namespaced so it can be used to issue `Certificates` across all namespaces. If you want to create a single `Issuer` that can be consumed in multiple namespaces, you should consider creating a `ClusterIssuer` resource. This is almost identical to the `Issuer` resource, however is non-namespaced so it can be used to issue `Certificates` across all namespaces.
You can read more about the `Issuer` and `ClusterIssuer` resources [here](https://cert-manager.io/docs/configuration/). You can read more about the `Issuer` and `ClusterIssuer` resources [here](https://cert-manager.io/docs/configuration/).
</Note> </Note>
</Step> </Step>
<Step title="Create Approver Policy">
If you create a `CertificateRequest` now, you'll notice it's neither approved nor denied. This is expected because by default cert-manager approver controller requires an approver-policy.
To enable approval, create the following YAML file and apply it:
```yaml infisical-approver-policy.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: infisical-issuer-approver
rules:
# Permission to approve or deny CertificateRequests for signers in cert-manager.io API group
- apiGroups: ['cert-manager.io']
resources: ['signers']
verbs: ['approve']
resourceNames:
# Grant approval permissions for namespaced issuers
- "issuers.infisical-issuer.infisical.com/default.issuer-infisical"
# Grant approval permissions for cluster-scoped issuers
- "clusterissuers.infisical-issuer.infisical.com/clusterissuer-infisical"
---
# Bind the cert-manager service account to the new role
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: infisical-issuer-approver-binding
subjects:
- kind: ServiceAccount
name: cert-manager
namespace: cert-manager
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: infisical-issuer-approver
```
```
kubectl apply -f infisical-approver-policy.yaml
```
This configuration creates a `ClusterRole` named `infisical-issuer-approver` that grants approval permissions for specific Infisical issuer types. It then binds this role to the cert-manager service account, allowing it to approve certificate requests from your Infisical issuers.
For information, check out [cert manager approval policy doc](https://cert-manager.io/docs/policy/approval/approver-policy/).
</Step>
<Step title="Create Certificate"> <Step title="Create Certificate">
Finally, create a `Certificate` by applying the following configuration file. Finally, create a `Certificate` by applying the following configuration file.
@@ -162,7 +198,7 @@ In the following steps, we explore how to install the Infisical PKI Issuer using
duration: 48h # the ttl for the certificate duration: 48h # the ttl for the certificate
renewBefore: 12h # the time before the certificate expiry that the certificate should be automatically renewed renewBefore: 12h # the time before the certificate expiry that the certificate should be automatically renewed
``` ```
The above sample configuration file specifies a certificate to be issued with the common name `certificate-by-issuer.example.com` and ECDSA private key using the P-256 curve, valid for 48 hours; the certificate will be automatically renewed by `cert-manager` 12 hours before expiry. The above sample configuration file specifies a certificate to be issued with the common name `certificate-by-issuer.example.com` and ECDSA private key using the P-256 curve, valid for 48 hours; the certificate will be automatically renewed by `cert-manager` 12 hours before expiry.
The certificate is issued by the issuer `issuer-infisical` created in the previous step and the resulting certificate and private key will be stored in a secret named `certificate-by-issuer`. The certificate is issued by the issuer `issuer-infisical` created in the previous step and the resulting certificate and private key will be stored in a secret named `certificate-by-issuer`.
@@ -181,7 +217,7 @@ In the following steps, we explore how to install the Infisical PKI Issuer using
</Step> </Step>
<Step title="Use Certificate in Kubernetes Secret"> <Step title="Use Certificate in Kubernetes Secret">
Since the actual certificate and private key are stored in a Kubernetes secret, we can check that the secret was created successfully by running the following command: Since the actual certificate and private key are stored in a Kubernetes secret, we can check that the secret was created successfully by running the following command:
```bash ```bash
kubectl get secret certificate-by-issuer -n <namespace_of_your_certificate> kubectl get secret certificate-by-issuer -n <namespace_of_your_certificate>
``` ```
@@ -190,9 +226,9 @@ In the following steps, we explore how to install the Infisical PKI Issuer using
NAME TYPE DATA AGE NAME TYPE DATA AGE
certificate-by-issuer kubernetes.io/tls 2 26h certificate-by-issuer kubernetes.io/tls 2 26h
``` ```
We can `describe` the secret to get more information about it: We can `describe` the secret to get more information about it:
```bash ```bash
kubectl describe secret certificate-by-issuer -n default kubectl describe secret certificate-by-issuer -n default
``` ```
@@ -201,14 +237,14 @@ In the following steps, we explore how to install the Infisical PKI Issuer using
Name: certificate-by-issuer Name: certificate-by-issuer
Namespace: default Namespace: default
Labels: controller.cert-manager.io/fao=true Labels: controller.cert-manager.io/fao=true
Annotations: cert-manager.io/alt-names: Annotations: cert-manager.io/alt-names:
cert-manager.io/certificate-name: certificate-by-issuer cert-manager.io/certificate-name: certificate-by-issuer
cert-manager.io/common-name: certificate-by-issuer.example.com cert-manager.io/common-name: certificate-by-issuer.example.com
cert-manager.io/ip-sans: cert-manager.io/ip-sans:
cert-manager.io/issuer-group: infisical-issuer.infisical.com cert-manager.io/issuer-group: infisical-issuer.infisical.com
cert-manager.io/issuer-kind: Issuer cert-manager.io/issuer-kind: Issuer
cert-manager.io/issuer-name: issuer-infisical cert-manager.io/issuer-name: issuer-infisical
cert-manager.io/uri-sans: cert-manager.io/uri-sans:
Type: kubernetes.io/tls Type: kubernetes.io/tls
@@ -218,17 +254,18 @@ In the following steps, we explore how to install the Infisical PKI Issuer using
tls.crt: 2380 bytes tls.crt: 2380 bytes
tls.key: 227 bytes tls.key: 227 bytes
``` ```
Here, `ca.crt` is the Root CA certificate, `tls.crt` is the requested certificate followed by the certificate chain, and `tls.key` is the private key for the certificate. Here, `ca.crt` is the Root CA certificate, `tls.crt` is the requested certificate followed by the certificate chain, and `tls.key` is the private key for the certificate.
We can decode the certificate and print it out using `openssl`: We can decode the certificate and print it out using `openssl`:
```bash ```bash
kubectl get secret certificate-by-issuer -n default -o jsonpath='{.data.tls\.crt}' | base64 --decode | openssl x509 -text -noout kubectl get secret certificate-by-issuer -n default -o jsonpath='{.data.tls\.crt}' | base64 --decode | openssl x509 -text -noout
``` ```
In any case, the certificate is ready to be used as Kubernetes Secret by your Kubernetes resources. In any case, the certificate is ready to be used as Kubernetes Secret by your Kubernetes resources.
</Step> </Step>
</Steps> </Steps>
## FAQ ## FAQ
@@ -236,15 +273,24 @@ In the following steps, we explore how to install the Infisical PKI Issuer using
<AccordionGroup> <AccordionGroup>
<Accordion title="What fields can be configured on the Certificate resource?"> <Accordion title="What fields can be configured on the Certificate resource?">
The full list of the fields supported on the `Certificate` resource can be found in the API reference documentation [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec). The full list of the fields supported on the `Certificate` resource can be found in the API reference documentation [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec).
<Note> <Note>
Currently, not all fields are supported by the Infisical PKI Issuer. Currently, not all fields are supported by the Infisical PKI Issuer.
</Note> </Note>
</Accordion> </Accordion>
<Accordion title="Can certificates be renewed automatically?"> <Accordion title="Can certificates be renewed automatically?">
Yes. `cert-manager` will automatically renew certificates according to the `renewBefore` threshold of expiry as Yes. `cert-manager` will automatically renew certificates according to the `renewBefore` threshold of expiry as
specified in the corresponding `Certificate` resource. specified in the corresponding `Certificate` resource.
You can read more about the `renewBefore` field [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec). You can read more about the `renewBefore` field [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec).
</Accordion> </Accordion>
</AccordionGroup> <Accordion title="Why is my CertificateRequest not being approved, showing 'CertificateRequest has not been approved yet. Ignoring.'?">
If you see log messages similar to:
```
"CertificateRequest has not been approved yet. Ignoring.","controller":"certificaterequest","controllerGroup":"cert-manager.io","controllerKind":"CertificateRequest","CertificateRequest":{"name":"skynet-infisical-rta-rsa2048-1","namespace":"infisical-system"},"namespace":"infisical-system","name":"skynet-infisical-rta-rsa2048-1","reconcileID":"bfb7cad9-d867-45b5-b3a3-0139e731b7a6"}
```
This indicates that the `CertificateRequest` has been created, but `cert-manager` has not yet approved it. This typically occurs because a necessary approver policy is missing. Refer to the documentation above to create an approver policy.
</Accordion>
</AccordionGroup>

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@ export {
ProjectPermissionKmipActions, ProjectPermissionKmipActions,
ProjectPermissionMemberActions, ProjectPermissionMemberActions,
ProjectPermissionPkiSubscriberActions, ProjectPermissionPkiSubscriberActions,
ProjectPermissionPkiTemplateActions,
ProjectPermissionSshHostActions, ProjectPermissionSshHostActions,
ProjectPermissionSub ProjectPermissionSub
} from "./types"; } from "./types";

View File

@@ -104,6 +104,15 @@ export enum ProjectPermissionPkiSubscriberActions {
ListCerts = "list-certs" ListCerts = "list-certs"
} }
export enum ProjectPermissionPkiTemplateActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
IssueCert = "issue-cert",
ListCerts = "list-certs"
}
export enum ProjectPermissionSecretRotationActions { export enum ProjectPermissionSecretRotationActions {
Read = "read", Read = "read",
ReadGeneratedCredentials = "read-generated-credentials", ReadGeneratedCredentials = "read-generated-credentials",
@@ -238,6 +247,11 @@ export type PkiSubscriberSubjectFields = {
name: string; name: string;
}; };
export type PkiTemplateSubjectFields = {
name: string;
// (dangtony98): consider adding [commonName] as a subject field in the future
};
export type ProjectPermissionSet = export type ProjectPermissionSet =
| [ | [
ProjectPermissionSecretActions, ProjectPermissionSecretActions,
@@ -295,7 +309,13 @@ export type ProjectPermissionSet =
] ]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates] | [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] | [
ProjectPermissionPkiTemplateActions,
(
| ProjectPermissionSub.CertificateTemplates
| (ForcedSubject<ProjectPermissionSub.CertificateTemplates> & PkiTemplateSubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]

View File

@@ -19,6 +19,7 @@ export {
ProjectPermissionKmipActions, ProjectPermissionKmipActions,
ProjectPermissionMemberActions, ProjectPermissionMemberActions,
ProjectPermissionPkiSubscriberActions, ProjectPermissionPkiSubscriberActions,
ProjectPermissionPkiTemplateActions,
ProjectPermissionSshHostActions, ProjectPermissionSshHostActions,
ProjectPermissionSub, ProjectPermissionSub,
useProjectPermission useProjectPermission

View File

@@ -1,8 +1,11 @@
export { export {
useCreateCertTemplate, useCreateCertTemplate,
useCreateCertTemplateV2,
useCreateEstConfig, useCreateEstConfig,
useDeleteCertTemplate, useDeleteCertTemplate,
useDeleteCertTemplateV2,
useUpdateCertTemplate, useUpdateCertTemplate,
useUpdateCertTemplateV2,
useUpdateEstConfig useUpdateEstConfig
} from "./mutations"; } from "./mutations";
export { useGetCertTemplate, useGetEstConfig } from "./queries"; export { useGetCertTemplate, useGetEstConfig, useListCertificateTemplates } from "./queries";

View File

@@ -8,9 +8,12 @@ import { certTemplateKeys } from "./queries";
import { import {
TCertificateTemplate, TCertificateTemplate,
TCreateCertificateTemplateDTO, TCreateCertificateTemplateDTO,
TCreateCertificateTemplateV2DTO,
TCreateEstConfigDTO, TCreateEstConfigDTO,
TDeleteCertificateTemplateDTO, TDeleteCertificateTemplateDTO,
TDeleteCertificateTemplateV2DTO,
TUpdateCertificateTemplateDTO, TUpdateCertificateTemplateDTO,
TUpdateCertificateTemplateV2DTO,
TUpdateEstConfigDTO TUpdateEstConfigDTO
} from "./types"; } from "./types";
@@ -73,6 +76,58 @@ export const useDeleteCertTemplate = () => {
}); });
}; };
export const useCreateCertTemplateV2 = () => {
const queryClient = useQueryClient();
return useMutation<TCertificateTemplate, object, TCreateCertificateTemplateV2DTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.post<{
certificateTemplate: TCertificateTemplate;
}>("/api/v2/pki/certificate-templates", dto);
return data.certificateTemplate;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
}
});
};
export const useUpdateCertTemplateV2 = () => {
const queryClient = useQueryClient();
return useMutation<TCertificateTemplate, object, TUpdateCertificateTemplateV2DTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.patch<{ certificateTemplate: TCertificateTemplate }>(
`/api/v2/pki/certificate-templates/${dto.templateName}`,
dto
);
return data.certificateTemplate;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
}
});
};
export const useDeleteCertTemplateV2 = () => {
const queryClient = useQueryClient();
return useMutation<TCertificateTemplate, object, TDeleteCertificateTemplateV2DTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.delete<{ certificateTemplate: TCertificateTemplate }>(
`/api/v2/pki/certificate-templates/${dto.templateName}`,
{
data: {
projectId: dto.projectId
}
}
);
return data.certificateTemplate;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
}
});
};
export const useCreateEstConfig = () => { export const useCreateEstConfig = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<object, object, TCreateEstConfigDTO>({ return useMutation<object, object, TCreateEstConfigDTO>({

View File

@@ -2,10 +2,20 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { TCertificateTemplate, TEstConfig } from "./types"; import {
TCertificateTemplate,
TCertificateTemplateV2,
TEstConfig,
TListCertificateTemplatesDTO
} from "./types";
export const certTemplateKeys = { export const certTemplateKeys = {
getCertTemplateById: (id: string) => [{ id }, "cert-template"], getCertTemplateById: (id: string) => [{ id }, "cert-template"],
listTemplates: ({ projectId, ...el }: { limit?: number; offset?: number; projectId: string }) => [
"list-template",
projectId,
el
],
getEstConfig: (id: string) => [{ id }, "cert-template-est-config"] getEstConfig: (id: string) => [{ id }, "cert-template-est-config"]
}; };
@@ -22,6 +32,29 @@ export const useGetCertTemplate = (id: string) => {
}); });
}; };
export const useListCertificateTemplates = ({
limit = 100,
offset = 0,
projectId
}: TListCertificateTemplatesDTO) => {
return useQuery({
queryKey: certTemplateKeys.listTemplates({ limit, offset, projectId }),
queryFn: async () => {
const { data } = await apiRequest.get<{
certificateTemplates: TCertificateTemplateV2[];
totalCount?: number;
}>("/api/v2/pki/certificate-templates", {
params: {
limit,
offset,
projectId
}
});
return data;
}
});
};
export const useGetEstConfig = (certificateTemplateId: string) => { export const useGetEstConfig = (certificateTemplateId: string) => {
return useQuery({ return useQuery({
queryKey: certTemplateKeys.getEstConfig(certificateTemplateId), queryKey: certTemplateKeys.getEstConfig(certificateTemplateId),

View File

@@ -14,6 +14,26 @@ export type TCertificateTemplate = {
extendedKeyUsages: CertExtendedKeyUsage[]; extendedKeyUsages: CertExtendedKeyUsage[];
}; };
export type TCertificateTemplateV2 = {
id: string;
caId: string;
caName: string;
projectId: string;
pkiCollectionId?: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
updatedAt: string;
createdAt: string;
ca: {
name: string;
id: string;
};
};
export type TCreateCertificateTemplateDTO = { export type TCreateCertificateTemplateDTO = {
caId: string; caId: string;
pkiCollectionId?: string; pkiCollectionId?: string;
@@ -44,6 +64,34 @@ export type TDeleteCertificateTemplateDTO = {
projectId: string; projectId: string;
}; };
export type TCreateCertificateTemplateV2DTO = {
caName: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
projectId: string;
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
};
export type TUpdateCertificateTemplateV2DTO = {
templateName: string;
caName?: string;
name?: string;
commonName?: string;
subjectAlternativeName?: string;
ttl?: string;
projectId: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
};
export type TDeleteCertificateTemplateV2DTO = {
templateName: string;
projectId: string;
};
export type TCreateEstConfigDTO = { export type TCreateEstConfigDTO = {
certificateTemplateId: string; certificateTemplateId: string;
caChain?: string; caChain?: string;
@@ -67,3 +115,9 @@ export type TEstConfig = {
isEnabled: boolean; isEnabled: boolean;
disableBootstrapCertValidation: boolean; disableBootstrapCertValidation: boolean;
}; };
export type TListCertificateTemplatesDTO = {
limit?: number;
offset?: number;
projectId: string;
};

View File

@@ -117,6 +117,24 @@ export const ProjectLayout = () => {
</MenuItem> </MenuItem>
)} )}
</Link> </Link>
<Link
to={
`/${ProjectType.CertificateManager}/$projectId/certificate-templates` as const
}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem
iconMode="reverse"
isSelected={isActive}
icon="pki-template"
>
Certificate Templates
</MenuItem>
)}
</Link>
<Link <Link
to={ to={
`/${ProjectType.CertificateManager}/$projectId/certificates` as const `/${ProjectType.CertificateManager}/$projectId/certificates` as const

View File

@@ -23,7 +23,6 @@ import { usePopUp } from "@app/hooks/usePopUp";
import { CaInstallCertModal } from "../CertificateAuthoritiesPage/components/CaInstallCertModal"; import { CaInstallCertModal } from "../CertificateAuthoritiesPage/components/CaInstallCertModal";
import { CaModal } from "../CertificateAuthoritiesPage/components/CaModal"; import { CaModal } from "../CertificateAuthoritiesPage/components/CaModal";
import { CertificateTemplatesSection } from "../CertificatesPage/components/CertificateTemplatesSection";
import { import {
CaCertificatesSection, CaCertificatesSection,
CaCrlsSection, CaCrlsSection,
@@ -126,7 +125,6 @@ const Page = () => {
</div> </div>
<div className="w-full"> <div className="w-full">
<CaCertificatesSection caId={data.id} /> <CaCertificatesSection caId={data.id} />
<CertificateTemplatesSection caId={data.id} />
<CaCrlsSection caId={data.id} /> <CaCrlsSection caId={data.id} />
</div> </div>
</div> </div>

View File

@@ -9,7 +9,11 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { DeleteActionModal, IconButton } from "@app/components/v2"; import { DeleteActionModal, IconButton } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import {
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub,
useWorkspace
} from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useDeleteCertTemplate } from "@app/hooks/api"; import { useDeleteCertTemplate } from "@app/hooks/api";
@@ -63,7 +67,7 @@ export const CertificateTemplatesSection = ({ caId }: Props) => {
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Certificate Templates</h3> <h3 className="text-lg font-semibold text-mineshaft-100">Certificate Templates</h3>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionPkiTemplateActions.Create}
a={ProjectPermissionSub.CertificateTemplates} a={ProjectPermissionSub.CertificateTemplates}
> >
{(isAllowed) => ( {(isAllowed) => (

View File

@@ -19,7 +19,11 @@ import {
Tooltip, Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context"; import {
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub,
useSubscription
} from "@app/context";
import { useGetCaCertTemplates } from "@app/hooks/api"; import { useGetCaCertTemplates } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -79,7 +83,7 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => {
Manage Policies Manage Policies
</DropdownMenuItem> </DropdownMenuItem>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionPkiTemplateActions.Edit}
a={ProjectPermissionSub.CertificateTemplates} a={ProjectPermissionSub.CertificateTemplates}
> >
{(isAllowed) => ( {(isAllowed) => (
@@ -105,7 +109,7 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => {
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionPkiTemplateActions.Delete}
a={ProjectPermissionSub.CertificateTemplates} a={ProjectPermissionSub.CertificateTemplates}
> >
{(isAllowed) => ( {(isAllowed) => (

View File

@@ -0,0 +1,257 @@
import { useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import {
faCertificate,
faEllipsis,
faPencil,
faPlus,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
Modal,
ModalContent,
PageHeader,
Pagination,
Table,
TableContainer,
TableSkeleton,
Tag,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import {
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub,
useWorkspace
} from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteCertTemplateV2 } from "@app/hooks/api";
import { useListCertificateTemplates } from "@app/hooks/api/certificateTemplates/queries";
import { PkiTemplateForm } from "./components/PkiTemplateForm";
const PER_PAGE_INIT = 25;
export const PkiTemplateListPage = () => {
const { t } = useTranslation();
const { currentWorkspace } = useWorkspace();
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"certificateTemplate",
"deleteTemplate"
] as const);
const { data, isPending } = useListCertificateTemplates({
projectId: currentWorkspace.id,
offset: (page - 1) * perPage,
limit: perPage
});
const deleteCertTemplate = useDeleteCertTemplateV2();
const onRemovePkiSubscriberSubmit = async () => {
try {
const pkiTemplate = await deleteCertTemplate.mutateAsync({
projectId: currentWorkspace.id,
templateName: popUp?.deleteTemplate?.data?.name
});
createNotification({
text: `Successfully deleted PKI template: ${pkiTemplate.name}`,
type: "success"
});
handlePopUpClose("deleteTemplate");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete PKI subscriber",
type: "error"
});
}
};
return (
<>
<Helmet>
<title>{t("common.head-title", { title: "PKI Subscribers" })}</title>
</Helmet>
<div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Certificate Templates"
description="Manage certificate template to request and issue dynamic certificates following a strict format."
/>
</div>
<div className="container mx-auto mb-6 max-w-7xl 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">Templates</p>
<div className="flex w-full justify-end">
<ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Create}
a={ProjectPermissionSub.CertificateTemplates}
>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("certificateTemplate")}
isDisabled={!isAllowed}
className="ml-4"
>
Add Template
</Button>
)}
</ProjectPermissionCan>
</div>
</div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Issuing CA</Th>
<Th className="w-64">Last Updated At</Th>
<Th />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="project-cert-templates" />}
{!isPending &&
data?.certificateTemplates?.map((template) => {
return (
<Tr className="h-10" key={`certificate-template-${template.id}`}>
<Td>{template.name}</Td>
<Td>
<Tag size="xs">{template.ca.name}</Tag>
</Td>
<Td>{format(new Date(template.updatedAt), "yyyy-MM-dd | HH:mm:ss")}</Td>
<Td className="text-right align-middle">
<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">
<ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Edit}
a={ProjectPermissionSub.CertificateTemplates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("certificateTemplate", template);
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faPencil} />}
>
Edit Template
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Delete}
a={ProjectPermissionSub.CertificateTemplates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteTemplate", template);
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete Template
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
{!isPending && !data?.certificateTemplates?.length && (
<Tr>
<Td colSpan={4}>
<EmptyState title="No certificate templates found" icon={faCertificate} />
</Td>
</Tr>
)}
</TBody>
</Table>
{!isPending && data?.totalCount !== undefined && data.totalCount >= PER_PAGE_INIT && (
<Pagination
count={data.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
</TableContainer>
<DeleteActionModal
isOpen={popUp.deleteTemplate.isOpen}
title="Are you sure you want to remove the PKI Template?"
onChange={(isOpen) => handlePopUpToggle("deleteTemplate", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => onRemovePkiSubscriberSubmit()}
/>
</div>
<div className="container mx-auto max-w-7xl" />
</div>
<Modal
isOpen={popUp?.certificateTemplate?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("certificateTemplate", isOpen)}
>
<ModalContent
title={
popUp.certificateTemplate?.data
? "Certificate Template"
: "Create Certificate Template"
}
>
<PkiTemplateForm
certTemplate={popUp?.certificateTemplate?.data}
handlePopUpToggle={(isOpen) => handlePopUpToggle("certificateTemplate", isOpen)}
/>
</ModalContent>
</Modal>
</div>
</>
);
};

View File

@@ -0,0 +1,408 @@
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 {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button,
Checkbox,
FilterableSelect,
FormControl,
FormLabel,
Input,
Tooltip
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
useCreateCertTemplateV2,
useListCasByProjectId,
useUpdateCertTemplateV2
} from "@app/hooks/api";
import {
EXTENDED_KEY_USAGES_OPTIONS,
KEY_USAGES_OPTIONS
} from "@app/hooks/api/certificates/constants";
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
import { TCertificateTemplateV2 } from "@app/hooks/api/certificateTemplates/types";
import { slugSchema } from "@app/lib/schemas";
const validateTemplateRegexField = z.string().trim().min(1).max(100);
const schema = z.object({
ca: z.object({
name: z.string(),
id: z.string()
}),
name: slugSchema(),
commonName: validateTemplateRegexField,
subjectAlternativeName: validateTemplateRegexField,
ttl: z.string().trim().min(1),
keyUsages: z.object({
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
[CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(),
[CertKeyUsage.NON_REPUDIATION]: z.boolean().optional(),
[CertKeyUsage.DATA_ENCIPHERMENT]: z.boolean().optional(),
[CertKeyUsage.KEY_AGREEMENT]: z.boolean().optional(),
[CertKeyUsage.KEY_CERT_SIGN]: z.boolean().optional(),
[CertKeyUsage.CRL_SIGN]: z.boolean().optional(),
[CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(),
[CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional()
}),
extendedKeyUsages: z.object({
[CertExtendedKeyUsage.CLIENT_AUTH]: z.boolean().optional(),
[CertExtendedKeyUsage.CODE_SIGNING]: z.boolean().optional(),
[CertExtendedKeyUsage.EMAIL_PROTECTION]: z.boolean().optional(),
[CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(),
[CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(),
[CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional()
})
});
export type FormData = z.infer<typeof schema>;
type Props = {
certTemplate?: TCertificateTemplateV2;
handlePopUpToggle: (state?: boolean) => void;
};
export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data: cas, isPending: isCaLoading } = useListCasByProjectId(currentWorkspace.id);
const { mutateAsync: createCertTemplate } = useCreateCertTemplateV2();
const { mutateAsync: updateCertTemplate } = useUpdateCertTemplateV2();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: async () => {
if (certTemplate) {
return {
ca: certTemplate.ca,
name: certTemplate.name,
commonName: certTemplate.commonName,
subjectAlternativeName: certTemplate.subjectAlternativeName,
ttl: certTemplate.ttl,
keyUsages: Object.fromEntries(certTemplate.keyUsages.map((name) => [name, true]) ?? []),
extendedKeyUsages: Object.fromEntries(
certTemplate.extendedKeyUsages.map((name) => [name, true]) ?? []
)
};
}
return {
ca: { name: "", id: "" },
name: "",
subjectAlternativeName: "",
commonName: "",
ttl: "",
keyUsages: {
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
[CertKeyUsage.KEY_ENCIPHERMENT]: true
},
extendedKeyUsages: {}
};
}
});
const onFormSubmit = async ({
name,
commonName,
subjectAlternativeName,
ttl,
keyUsages,
extendedKeyUsages,
ca
}: FormData) => {
if (!currentWorkspace?.id) {
return;
}
try {
if (certTemplate) {
await updateCertTemplate({
templateName: certTemplate.name,
projectId: currentWorkspace.id,
caName: ca.name,
name,
commonName,
subjectAlternativeName,
ttl,
keyUsages: Object.entries(keyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertKeyUsage),
extendedKeyUsages: Object.entries(extendedKeyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertExtendedKeyUsage)
});
createNotification({
text: "Successfully updated certificate template",
type: "success"
});
} else {
await createCertTemplate({
projectId: currentWorkspace.id,
caName: ca.name,
name,
commonName,
subjectAlternativeName,
ttl,
keyUsages: Object.entries(keyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertKeyUsage),
extendedKeyUsages: Object.entries(extendedKeyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertExtendedKeyUsage)
});
createNotification({
text: "Successfully created certificate template",
type: "success"
});
}
reset();
handlePopUpToggle(false);
} catch (err) {
console.error(err);
createNotification({
text: "Failed to save changes",
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
{certTemplate && (
<FormControl label="Certificate Template ID">
<Input value={certTemplate.id} isDisabled className="bg-white/[0.07]" />
</FormControl>
)}
<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-template" />
</FormControl>
)}
/>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Issuing CA"
errorText={error?.message}
isError={Boolean(error)}
isRequired
>
<FilterableSelect
options={cas || []}
isLoading={isCaLoading}
placeholder="Select CA..."
onChange={onChange}
value={value}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
control={control}
name="ca"
/>
<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>
)}
/>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="key-usages" className="data-[state=open]:border-none">
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
<div className="order-1 ml-3">Key Usage</div>
</AccordionTrigger>
<AccordionContent>
<Controller
control={control}
name="keyUsages"
render={({ field: { onChange, value }, fieldState: { error } }) => {
return (
<FormControl
label="Key Usage"
errorText={error?.message}
isError={Boolean(error)}
>
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
{KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
return (
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
...value,
[optionValue]: state
});
}}
>
{label}
</Checkbox>
);
})}
</div>
</FormControl>
);
}}
/>
<Controller
control={control}
name="extendedKeyUsages"
render={({ field: { onChange, value }, fieldState: { error } }) => {
return (
<FormControl
label="Extended Key Usage"
errorText={error?.message}
isError={Boolean(error)}
>
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
{EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
return (
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
...value,
[optionValue]: state
});
}}
>
{label}
</Checkbox>
);
})}
</div>
</FormControl>
);
}}
/>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="mt-4 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Save
</Button>
<Button colorSchema="secondary" variant="plain" onClick={() => handlePopUpToggle(false)}>
Cancel
</Button>
</div>
</form>
);
};

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { PkiTemplateListPage } from "./PkiTemplateListPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates/"
)({
component: PkiTemplateListPage
});

View File

@@ -0,0 +1,173 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = {
position?: number;
isDisabled?: boolean;
};
export const PkiTemplatePermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
formState: { errors }
} = useFormContext<TFormSchema>();
const permissionSubject = ProjectPermissionSub.CertificateTemplates;
const items = useFieldArray({
control,
name: `permissions.${permissionSubject}.${position}.conditions`
});
return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
<p className="mt-2 text-gray-300">Conditions</p>
<p className="text-sm text-mineshaft-400">
Conditions determine when a policy will be applied (always if no conditions are present).
</p>
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
All conditions must evaluate to true for the policy to take effect.
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const condition =
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value="name">Name</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value={PermissionConditionOperators.$EQ}>Equals</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</Select>
</FormControl>
)}
/>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="delete"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
</div>
)}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "name",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
);
};

View File

@@ -18,6 +18,7 @@ import {
ProjectPermissionKmipActions, ProjectPermissionKmipActions,
ProjectPermissionMemberActions, ProjectPermissionMemberActions,
ProjectPermissionPkiSubscriberActions, ProjectPermissionPkiSubscriberActions,
ProjectPermissionPkiTemplateActions,
ProjectPermissionSecretActions, ProjectPermissionSecretActions,
ProjectPermissionSecretRotationActions, ProjectPermissionSecretRotationActions,
ProjectPermissionSecretSyncActions, ProjectPermissionSecretSyncActions,
@@ -148,6 +149,15 @@ const PkiSubscriberPolicyActionSchema = z.object({
[ProjectPermissionPkiSubscriberActions.ListCerts]: z.boolean().optional() [ProjectPermissionPkiSubscriberActions.ListCerts]: z.boolean().optional()
}); });
const PkiTemplatePolicyActionSchema = z.object({
[ProjectPermissionPkiTemplateActions.Read]: z.boolean().optional(),
[ProjectPermissionPkiTemplateActions.Create]: z.boolean().optional(),
[ProjectPermissionPkiTemplateActions.Edit]: z.boolean().optional(),
[ProjectPermissionPkiTemplateActions.Delete]: z.boolean().optional(),
[ProjectPermissionPkiTemplateActions.IssueCert]: z.boolean().optional(),
[ProjectPermissionPkiTemplateActions.ListCerts]: z.boolean().optional()
});
const SecretRollbackPolicyActionSchema = z.object({ const SecretRollbackPolicyActionSchema = z.object({
read: z.boolean().optional(), read: z.boolean().optional(),
create: z.boolean().optional() create: z.boolean().optional()
@@ -255,7 +265,12 @@ export const projectRoleFormSchema = z.object({
.default([]), .default([]),
[ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.CertificateTemplates]: PkiTemplatePolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default( [ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default(
[] []
), ),
@@ -295,6 +310,7 @@ type TConditionalFields =
| ProjectPermissionSub.SecretImports | ProjectPermissionSub.SecretImports
| ProjectPermissionSub.DynamicSecrets | ProjectPermissionSub.DynamicSecrets
| ProjectPermissionSub.PkiSubscribers | ProjectPermissionSub.PkiSubscribers
| ProjectPermissionSub.CertificateTemplates
| ProjectPermissionSub.SshHosts | ProjectPermissionSub.SshHosts
| ProjectPermissionSub.SecretRotation | ProjectPermissionSub.SecretRotation
| ProjectPermissionSub.Identity; | ProjectPermissionSub.Identity;
@@ -309,7 +325,8 @@ export const isConditionalSubjects = (
subject === ProjectPermissionSub.Identity || subject === ProjectPermissionSub.Identity ||
subject === ProjectPermissionSub.SshHosts || subject === ProjectPermissionSub.SshHosts ||
subject === ProjectPermissionSub.SecretRotation || subject === ProjectPermissionSub.SecretRotation ||
subject === ProjectPermissionSub.PkiSubscribers; subject === ProjectPermissionSub.PkiSubscribers ||
subject === ProjectPermissionSub.CertificateTemplates;
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => { const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
const formConditions: z.infer<typeof ConditionSchema> = []; const formConditions: z.infer<typeof ConditionSchema> = [];
@@ -408,7 +425,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
ProjectPermissionSub.CertificateAuthorities, ProjectPermissionSub.CertificateAuthorities,
ProjectPermissionSub.PkiAlerts, ProjectPermissionSub.PkiAlerts,
ProjectPermissionSub.PkiCollections, ProjectPermissionSub.PkiCollections,
ProjectPermissionSub.CertificateTemplates,
ProjectPermissionSub.Tags, ProjectPermissionSub.Tags,
ProjectPermissionSub.SecretRotation, ProjectPermissionSub.SecretRotation,
ProjectPermissionSub.Kms, ProjectPermissionSub.Kms,
@@ -781,6 +797,34 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted inverted
}); });
return;
}
if (subject === ProjectPermissionSub.CertificateTemplates) {
if (!formVal[subject]) formVal[subject] = [];
formVal[subject]!.push({
[ProjectPermissionPkiTemplateActions.Edit]: action.includes(
ProjectPermissionPkiTemplateActions.Edit
),
[ProjectPermissionPkiTemplateActions.Delete]: action.includes(
ProjectPermissionPkiTemplateActions.Delete
),
[ProjectPermissionPkiTemplateActions.Create]: action.includes(
ProjectPermissionPkiTemplateActions.Create
),
[ProjectPermissionPkiTemplateActions.Read]: action.includes(
ProjectPermissionPkiTemplateActions.Read
),
[ProjectPermissionPkiTemplateActions.IssueCert]: action.includes(
ProjectPermissionPkiTemplateActions.IssueCert
),
[ProjectPermissionPkiTemplateActions.ListCerts]: action.includes(
ProjectPermissionPkiTemplateActions.ListCerts
),
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted
});
} }
}); });
@@ -1119,10 +1163,12 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.CertificateTemplates]: { [ProjectPermissionSub.CertificateTemplates]: {
title: "Certificate Templates", title: "Certificate Templates",
actions: [ actions: [
{ label: "Read", value: "read" }, { label: "Read", value: ProjectPermissionPkiTemplateActions.Read },
{ label: "Create", value: "create" }, { label: "Create", value: ProjectPermissionPkiTemplateActions.Create },
{ label: "Modify", value: "edit" }, { label: "Modify", value: ProjectPermissionPkiTemplateActions.Edit },
{ label: "Remove", value: "delete" } { label: "Remove", value: ProjectPermissionPkiTemplateActions.Delete },
{ label: "Issue Certificates", value: ProjectPermissionPkiTemplateActions.IssueCert },
{ label: "List Certificates", value: ProjectPermissionPkiTemplateActions.ListCerts }
] ]
}, },
[ProjectPermissionSub.SshCertificateAuthorities]: { [ProjectPermissionSub.SshCertificateAuthorities]: {

View File

@@ -23,6 +23,7 @@ import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions"; import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions";
import { PermissionEmptyState } from "./PermissionEmptyState"; import { PermissionEmptyState } from "./PermissionEmptyState";
import { PkiSubscriberPermissionConditions } from "./PkiSubscriberPermissionConditions"; import { PkiSubscriberPermissionConditions } from "./PkiSubscriberPermissionConditions";
import { PkiTemplatePermissionConditions } from "./PkiTemplatePermissionConditions";
import { import {
formRolePermission2API, formRolePermission2API,
isConditionalSubjects, isConditionalSubjects,
@@ -63,6 +64,10 @@ export const renderConditionalComponents = (
return <PkiSubscriberPermissionConditions isDisabled={isDisabled} />; return <PkiSubscriberPermissionConditions isDisabled={isDisabled} />;
} }
if (subject === ProjectPermissionSub.CertificateTemplates) {
return <PkiTemplatePermissionConditions isDisabled={isDisabled} />;
}
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />; return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
} }

View File

@@ -123,6 +123,7 @@ import { Route as certManagerPkiSubscriberDetailsByIDPageRouteImport } from './p
import { Route as certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route' import { Route as certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route'
import { Route as secretManagerIntegrationsListPageRouteImport } from './pages/secret-manager/IntegrationsListPage/route' import { Route as secretManagerIntegrationsListPageRouteImport } from './pages/secret-manager/IntegrationsListPage/route'
import { Route as certManagerPkiSubscribersPageRouteImport } from './pages/cert-manager/PkiSubscribersPage/route' import { Route as certManagerPkiSubscribersPageRouteImport } from './pages/cert-manager/PkiSubscribersPage/route'
import { Route as certManagerPkiTemplateListPageRouteImport } from './pages/cert-manager/PkiTemplateListPage/route'
import { Route as secretManagerIntegrationsWindmillConfigurePageRouteImport } from './pages/secret-manager/integrations/WindmillConfigurePage/route' import { Route as secretManagerIntegrationsWindmillConfigurePageRouteImport } from './pages/secret-manager/integrations/WindmillConfigurePage/route'
import { Route as secretManagerIntegrationsWindmillAuthorizePageRouteImport } from './pages/secret-manager/integrations/WindmillAuthorizePage/route' import { Route as secretManagerIntegrationsWindmillAuthorizePageRouteImport } from './pages/secret-manager/integrations/WindmillAuthorizePage/route'
import { Route as secretManagerIntegrationsVercelConfigurePageRouteImport } from './pages/secret-manager/integrations/VercelConfigurePage/route' import { Route as secretManagerIntegrationsVercelConfigurePageRouteImport } from './pages/secret-manager/integrations/VercelConfigurePage/route'
@@ -257,6 +258,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayout
createFileRoute( createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers', '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers',
)() )()
const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates',
)()
// Create/Update Routes // Create/Update Routes
@@ -870,6 +875,15 @@ const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayout
} as any, } as any,
) )
const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRoute =
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesImport.update(
{
id: '/certificate-templates',
path: '/certificate-templates',
getParentRoute: () => certManagerLayoutRoute,
} as any,
)
const projectAccessControlPageRouteCertManagerRoute = const projectAccessControlPageRouteCertManagerRoute =
projectAccessControlPageRouteCertManagerImport.update({ projectAccessControlPageRouteCertManagerImport.update({
id: '/access-management', id: '/access-management',
@@ -1156,6 +1170,14 @@ const certManagerPkiSubscribersPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute, AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute,
} as any) } as any)
const certManagerPkiTemplateListPageRouteRoute =
certManagerPkiTemplateListPageRouteImport.update({
id: '/',
path: '/',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRoute,
} as any)
const secretManagerIntegrationsWindmillConfigurePageRouteRoute = const secretManagerIntegrationsWindmillConfigurePageRouteRoute =
secretManagerIntegrationsWindmillConfigurePageRouteImport.update({ secretManagerIntegrationsWindmillConfigurePageRouteImport.update({
id: '/windmill/create', id: '/windmill/create',
@@ -2391,6 +2413,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof projectAccessControlPageRouteCertManagerImport preLoaderRoute: typeof projectAccessControlPageRouteCertManagerImport
parentRoute: typeof certManagerLayoutImport parentRoute: typeof certManagerLayoutImport
} }
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates': {
id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates'
path: '/certificate-templates'
fullPath: '/cert-manager/$projectId/certificate-templates'
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesImport
parentRoute: typeof certManagerLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers': { '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers': {
id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers' id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers'
path: '/subscribers' path: '/subscribers'
@@ -2489,6 +2518,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof projectAccessControlPageRouteSshImport preLoaderRoute: typeof projectAccessControlPageRouteSshImport
parentRoute: typeof sshLayoutImport parentRoute: typeof sshLayoutImport
} }
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates/': {
id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates/'
path: '/'
fullPath: '/cert-manager/$projectId/certificate-templates/'
preLoaderRoute: typeof certManagerPkiTemplateListPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesImport
}
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/': { '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/': {
id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/' id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/'
path: '/' path: '/'
@@ -3360,6 +3396,21 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteWithChildren =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren, AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren,
) )
interface AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteChildren {
certManagerPkiTemplateListPageRouteRoute: typeof certManagerPkiTemplateListPageRouteRoute
}
const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteChildren =
{
certManagerPkiTemplateListPageRouteRoute:
certManagerPkiTemplateListPageRouteRoute,
}
const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren =
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRoute._addFileChildren(
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteChildren { interface AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteChildren {
certManagerPkiSubscribersPageRouteRoute: typeof certManagerPkiSubscribersPageRouteRoute certManagerPkiSubscribersPageRouteRoute: typeof certManagerPkiSubscribersPageRouteRoute
certManagerPkiSubscriberDetailsByIDPageRouteRoute: typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute certManagerPkiSubscriberDetailsByIDPageRouteRoute: typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute
@@ -3384,6 +3435,7 @@ interface certManagerLayoutRouteChildren {
certManagerCertificatesPageRouteRoute: typeof certManagerCertificatesPageRouteRoute certManagerCertificatesPageRouteRoute: typeof certManagerCertificatesPageRouteRoute
certManagerSettingsPageRouteRoute: typeof certManagerSettingsPageRouteRoute certManagerSettingsPageRouteRoute: typeof certManagerSettingsPageRouteRoute
projectAccessControlPageRouteCertManagerRoute: typeof projectAccessControlPageRouteCertManagerRoute projectAccessControlPageRouteCertManagerRoute: typeof projectAccessControlPageRouteCertManagerRoute
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren
certManagerCertAuthDetailsByIDPageRouteRoute: typeof certManagerCertAuthDetailsByIDPageRouteRoute certManagerCertAuthDetailsByIDPageRouteRoute: typeof certManagerCertAuthDetailsByIDPageRouteRoute
projectIdentityDetailsByIDPageRouteCertManagerRoute: typeof projectIdentityDetailsByIDPageRouteCertManagerRoute projectIdentityDetailsByIDPageRouteCertManagerRoute: typeof projectIdentityDetailsByIDPageRouteCertManagerRoute
@@ -3400,6 +3452,8 @@ const certManagerLayoutRouteChildren: certManagerLayoutRouteChildren = {
certManagerSettingsPageRouteRoute: certManagerSettingsPageRouteRoute, certManagerSettingsPageRouteRoute: certManagerSettingsPageRouteRoute,
projectAccessControlPageRouteCertManagerRoute: projectAccessControlPageRouteCertManagerRoute:
projectAccessControlPageRouteCertManagerRoute, projectAccessControlPageRouteCertManagerRoute,
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRoute:
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute: AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute:
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren, AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren,
certManagerCertAuthDetailsByIDPageRouteRoute: certManagerCertAuthDetailsByIDPageRouteRoute:
@@ -4089,6 +4143,7 @@ export interface FileRoutesByFullPath {
'/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute '/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute
'/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute '/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute
'/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute '/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute
'/cert-manager/$projectId/certificate-templates': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren
'/cert-manager/$projectId/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren '/cert-manager/$projectId/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren
'/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute '/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute
'/integrations/azure-key-vault/oauth2/callback': typeof secretManagerIntegrationsRouteAzureKeyVaultOauthRedirectRoute '/integrations/azure-key-vault/oauth2/callback': typeof secretManagerIntegrationsRouteAzureKeyVaultOauthRedirectRoute
@@ -4103,6 +4158,7 @@ export interface FileRoutesByFullPath {
'/secret-manager/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute '/secret-manager/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
'/secret-manager/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren '/secret-manager/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren
'/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute '/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
'/cert-manager/$projectId/certificate-templates/': typeof certManagerPkiTemplateListPageRouteRoute
'/cert-manager/$projectId/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute '/cert-manager/$projectId/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute
'/secret-manager/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute '/secret-manager/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
'/cert-manager/$projectId/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute '/cert-manager/$projectId/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute
@@ -4288,6 +4344,7 @@ export interface FileRoutesByTo {
'/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute '/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute
'/secret-manager/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute '/secret-manager/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
'/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute '/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
'/cert-manager/$projectId/certificate-templates': typeof certManagerPkiTemplateListPageRouteRoute
'/cert-manager/$projectId/subscribers': typeof certManagerPkiSubscribersPageRouteRoute '/cert-manager/$projectId/subscribers': typeof certManagerPkiSubscribersPageRouteRoute
'/secret-manager/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute '/secret-manager/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute
'/cert-manager/$projectId/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute '/cert-manager/$projectId/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute
@@ -4479,6 +4536,7 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview': typeof sshSshHostsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview': typeof sshSshHostsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings': typeof sshSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings': typeof sshSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management': typeof projectAccessControlPageRouteCertManagerRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management': typeof projectAccessControlPageRouteCertManagerRoute
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute '/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute
'/_authenticate/_inject-org-details/_org-layout/integrations/azure-key-vault/oauth2/callback': typeof secretManagerIntegrationsRouteAzureKeyVaultOauthRedirectRoute '/_authenticate/_inject-org-details/_org-layout/integrations/azure-key-vault/oauth2/callback': typeof secretManagerIntegrationsRouteAzureKeyVaultOauthRedirectRoute
@@ -4493,6 +4551,7 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/access-management': typeof projectAccessControlPageRouteSecretManagerRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management': typeof projectAccessControlPageRouteSshRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management': typeof projectAccessControlPageRouteSshRoute
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates/': typeof certManagerPkiTemplateListPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/': typeof secretManagerIntegrationsListPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute
@@ -4676,6 +4735,7 @@ export interface FileRouteTypes {
| '/ssh/$projectId/overview' | '/ssh/$projectId/overview'
| '/ssh/$projectId/settings' | '/ssh/$projectId/settings'
| '/cert-manager/$projectId/access-management' | '/cert-manager/$projectId/access-management'
| '/cert-manager/$projectId/certificate-templates'
| '/cert-manager/$projectId/subscribers' | '/cert-manager/$projectId/subscribers'
| '/integrations/azure-app-configuration/oauth2/callback' | '/integrations/azure-app-configuration/oauth2/callback'
| '/integrations/azure-key-vault/oauth2/callback' | '/integrations/azure-key-vault/oauth2/callback'
@@ -4690,6 +4750,7 @@ export interface FileRouteTypes {
| '/secret-manager/$projectId/access-management' | '/secret-manager/$projectId/access-management'
| '/secret-manager/$projectId/integrations' | '/secret-manager/$projectId/integrations'
| '/ssh/$projectId/access-management' | '/ssh/$projectId/access-management'
| '/cert-manager/$projectId/certificate-templates/'
| '/cert-manager/$projectId/subscribers/' | '/cert-manager/$projectId/subscribers/'
| '/secret-manager/$projectId/integrations/' | '/secret-manager/$projectId/integrations/'
| '/cert-manager/$projectId/ca/$caName' | '/cert-manager/$projectId/ca/$caName'
@@ -4874,6 +4935,7 @@ export interface FileRouteTypes {
| '/kms/$projectId/access-management' | '/kms/$projectId/access-management'
| '/secret-manager/$projectId/access-management' | '/secret-manager/$projectId/access-management'
| '/ssh/$projectId/access-management' | '/ssh/$projectId/access-management'
| '/cert-manager/$projectId/certificate-templates'
| '/cert-manager/$projectId/subscribers' | '/cert-manager/$projectId/subscribers'
| '/secret-manager/$projectId/integrations' | '/secret-manager/$projectId/integrations'
| '/cert-manager/$projectId/ca/$caName' | '/cert-manager/$projectId/ca/$caName'
@@ -5063,6 +5125,7 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview'
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings'
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management'
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates'
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers'
| '/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback' | '/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback'
| '/_authenticate/_inject-org-details/_org-layout/integrations/azure-key-vault/oauth2/callback' | '/_authenticate/_inject-org-details/_org-layout/integrations/azure-key-vault/oauth2/callback'
@@ -5077,6 +5140,7 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/access-management' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/access-management'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations'
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management'
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates/'
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/'
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName'
@@ -5605,6 +5669,7 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates",
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings",
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management",
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates",
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers",
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName",
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId",
@@ -5731,6 +5796,13 @@ export const routeTree = rootRoute
"filePath": "project/AccessControlPage/route-cert-manager.tsx", "filePath": "project/AccessControlPage/route-cert-manager.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout"
}, },
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout",
"children": [
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates/"
]
},
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers": { "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers": {
"filePath": "", "filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout",
@@ -5871,6 +5943,10 @@ export const routeTree = rootRoute
"filePath": "project/AccessControlPage/route-ssh.tsx", "filePath": "project/AccessControlPage/route-ssh.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout" "parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout"
}, },
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates/": {
"filePath": "cert-manager/PkiTemplateListPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates"
},
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/": { "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/": {
"filePath": "cert-manager/PkiSubscribersPage/route.tsx", "filePath": "cert-manager/PkiSubscribersPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers" "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers"

View File

@@ -293,6 +293,7 @@ const certManagerRoutes = route("/cert-manager/$projectId", [
index("cert-manager/PkiSubscribersPage/route.tsx"), index("cert-manager/PkiSubscribersPage/route.tsx"),
route("/$subscriberName", "cert-manager/PkiSubscriberDetailsByIDPage/route.tsx") route("/$subscriberName", "cert-manager/PkiSubscriberDetailsByIDPage/route.tsx")
]), ]),
route("/certificate-templates", [index("cert-manager/PkiTemplateListPage/route.tsx")]),
route("/certificates", "cert-manager/CertificatesPage/route.tsx"), route("/certificates", "cert-manager/CertificatesPage/route.tsx"),
route("/certificate-authorities", "cert-manager/CertificateAuthoritiesPage/route.tsx"), route("/certificate-authorities", "cert-manager/CertificateAuthoritiesPage/route.tsx"),
route("/alerting", "cert-manager/AlertingPage/route.tsx"), route("/alerting", "cert-manager/AlertingPage/route.tsx"),