mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #3686 from akhilmhdh/feat/template-k8-issuer
Feat/template k8 issuer
This commit is contained in:
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -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
|
||||||
|
|||||||
@@ -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> {}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
309
backend/src/server/routes/v2/pki-templates-router.ts
Normal file
309
backend/src/server/routes/v2/pki-templates-router.ts
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
};
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|||||||
@@ -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 })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
102
backend/src/services/pki-templates/pki-templates-dal.ts
Normal file
102
backend/src/services/pki-templates/pki-templates-dal.ts
Normal 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 };
|
||||||
|
};
|
||||||
644
backend/src/services/pki-templates/pki-templates-service.ts
Normal file
644
backend/src/services/pki-templates/pki-templates-service.ts
Normal 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
|
||||||
|
};
|
||||||
|
};
|
||||||
53
backend/src/services/pki-templates/pki-templates-types.ts
Normal file
53
backend/src/services/pki-templates/pki-templates-types.ts
Normal 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;
|
||||||
@@ -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 })
|
||||||
|
)
|
||||||
|
)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
3098
frontend/public/lotties/pki-template.json
Normal file
3098
frontend/public/lotties/pki-template.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ export {
|
|||||||
ProjectPermissionKmipActions,
|
ProjectPermissionKmipActions,
|
||||||
ProjectPermissionMemberActions,
|
ProjectPermissionMemberActions,
|
||||||
ProjectPermissionPkiSubscriberActions,
|
ProjectPermissionPkiSubscriberActions,
|
||||||
|
ProjectPermissionPkiTemplateActions,
|
||||||
ProjectPermissionSshHostActions,
|
ProjectPermissionSshHostActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export {
|
|||||||
ProjectPermissionKmipActions,
|
ProjectPermissionKmipActions,
|
||||||
ProjectPermissionMemberActions,
|
ProjectPermissionMemberActions,
|
||||||
ProjectPermissionPkiSubscriberActions,
|
ProjectPermissionPkiSubscriberActions,
|
||||||
|
ProjectPermissionPkiTemplateActions,
|
||||||
ProjectPermissionSshHostActions,
|
ProjectPermissionSshHostActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProjectPermission
|
useProjectPermission
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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>({
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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]: {
|
||||||
|
|||||||
@@ -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} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
Reference in New Issue
Block a user