From 86862b932cda379e6bd89725b94b7b2570247973 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 30 May 2025 14:40:25 +0530 Subject: [PATCH 1/9] feat: completed backend changes for new pki template --- backend/src/@types/fastify.d.ts | 2 + .../20250528145356_add-template-slug.ts | 24 + .../ee/services/oidc/oidc-config-service.ts | 1 + .../ee/services/permission/default-roles.ts | 27 +- .../services/permission/project-permission.ts | 54 +- backend/src/server/routes/index.ts | 19 + backend/src/server/routes/v2/index.ts | 11 +- .../server/routes/v2/pki-templates-router.ts | 309 +++++++++ .../internal-certificate-authority-fns.ts | 274 +++++++- .../internal-certificate-authority-service.ts | 15 +- .../internal-certificate-authority-types.ts | 10 + .../certificate-template-schema.ts | 17 + .../certificate-template-service.ts | 42 +- .../pki-templates/pki-templates-dal.ts | 102 +++ .../pki-templates/pki-templates-service.ts | 624 ++++++++++++++++++ .../pki-templates/pki-templates-types.ts | 53 ++ .../src/services/project/project-service.ts | 13 +- 17 files changed, 1552 insertions(+), 45 deletions(-) create mode 100644 backend/src/db/migrations/20250528145356_add-template-slug.ts create mode 100644 backend/src/server/routes/v2/pki-templates-router.ts create mode 100644 backend/src/services/pki-templates/pki-templates-dal.ts create mode 100644 backend/src/services/pki-templates/pki-templates-service.ts create mode 100644 backend/src/services/pki-templates/pki-templates-types.ts diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index a32ed56a3..3fcb06fbf 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -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 { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-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 { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service"; @@ -271,6 +272,7 @@ declare module "fastify" { assumePrivileges: TAssumePrivilegeServiceFactory; githubOrgSync: TGithubOrgSyncServiceFactory; internalCertificateAuthority: TInternalCertificateAuthorityServiceFactory; + pkiTemplate: TPkiTemplatesServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/db/migrations/20250528145356_add-template-slug.ts b/backend/src/db/migrations/20250528145356_add-template-slug.ts new file mode 100644 index 000000000..34a7e38f8 --- /dev/null +++ b/backend/src/db/migrations/20250528145356_add-template-slug.ts @@ -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 { + 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 {} diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index d933835e4..7cebc1825 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -15,6 +15,7 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/pe import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index a3f444a11..f0993b30c 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -10,6 +10,7 @@ import { ProjectPermissionKmipActions, ProjectPermissionMemberActions, ProjectPermissionPkiSubscriberActions, + ProjectPermissionPkiTemplateActions, ProjectPermissionSecretActions, ProjectPermissionSecretRotationActions, ProjectPermissionSecretSyncActions, @@ -35,7 +36,6 @@ const buildAdminPermissionRules = () => { ProjectPermissionSub.AuditLogs, ProjectPermissionSub.IpAllowList, ProjectPermissionSub.CertificateAuthorities, - ProjectPermissionSub.CertificateTemplates, ProjectPermissionSub.PkiAlerts, ProjectPermissionSub.PkiCollections, ProjectPermissionSub.SshCertificateAuthorities, @@ -56,10 +56,24 @@ const buildAdminPermissionRules = () => { can( [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete + ProjectPermissionPkiTemplateActions.Read, + ProjectPermissionPkiTemplateActions.Edit, + ProjectPermissionPkiTemplateActions.Create, + ProjectPermissionPkiTemplateActions.Delete, + ProjectPermissionPkiTemplateActions.IssueCert, + ProjectPermissionPkiTemplateActions.ListCerts + ], + ProjectPermissionSub.CertificateTemplates + ); + + can( + [ + ProjectPermissionApprovalActions.Read, + ProjectPermissionApprovalActions.Edit, + ProjectPermissionApprovalActions.Create, + ProjectPermissionApprovalActions.Delete, + ProjectPermissionApprovalActions.AllowChangeBypass, + ProjectPermissionApprovalActions.AllowAccessBypass ], ProjectPermissionSub.SecretApproval ); @@ -348,7 +362,7 @@ const buildMemberPermissionRules = () => { ProjectPermissionSub.Certificates ); - can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateTemplates); + can([ProjectPermissionPkiTemplateActions.Read], ProjectPermissionSub.CertificateTemplates); can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts); can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections); @@ -417,6 +431,7 @@ const buildViewerPermissionRules = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities); can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates); + can(ProjectPermissionPkiTemplateActions.Read, ProjectPermissionSub.CertificateTemplates); can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates); diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 14408e8a0..48fae49cd 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -87,6 +87,15 @@ export enum ProjectPermissionSshHostActions { IssueHostCert = "issue-host-cert" } +export enum ProjectPermissionPkiTemplateActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + IssueCert = "issue-cert", + ListCerts = "list-certs" +} + export enum ProjectPermissionPkiSubscriberActions { Read = "read", Create = "create", @@ -200,6 +209,11 @@ export type SshHostSubjectFields = { hostname: string; }; +export type PkiTemplateSubjectFields = { + name: string; + // (dangtony98): consider adding [commonName] as a subject field in the future +}; + export type PkiSubscriberSubjectFields = { name: string; // (dangtony98): consider adding [commonName] as a subject field in the future @@ -256,7 +270,13 @@ export type ProjectPermissionSet = ] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates] - | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] + | [ + ProjectPermissionPkiTemplateActions, + ( + | ProjectPermissionSub.CertificateTemplates + | (ForcedSubject & PkiTemplateSubjectFields) + ) + ] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] @@ -436,6 +456,21 @@ const PkiSubscriberConditionSchema = z }) .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 = [ z.object({ 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." ) }), - 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({ subject: z .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." ).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({ subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."), 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." ).optional() }), + ...GeneralPermissionSchema ]); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 2b788bb2b..5971eb457 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -211,6 +211,8 @@ import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-co import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; import { pkiSubscriberQueueServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-queue"; 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 { projectQueueFactory } from "@app/services/project/project-queue"; import { projectServiceFactory } from "@app/services/project/project-service"; @@ -847,6 +849,7 @@ export const registerRoutes = async ( const pkiCollectionDAL = pkiCollectionDALFactory(db); const pkiCollectionItemDAL = pkiCollectionItemDALFactory(db); const pkiSubscriberDAL = pkiSubscriberDALFactory(db); + const pkiTemplatesDAL = pkiTemplatesDALFactory(db); const certificateService = certificateServiceFactory({ certificateDAL, @@ -1754,6 +1757,21 @@ export const registerRoutes = async ( internalCaFns }); + const pkiTemplateService = pkiTemplatesServiceFactory({ + pkiTemplatesDAL, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + certificateAuthoritySecretDAL, + certificateAuthorityCrlDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + projectDAL, + kmsService, + permissionService, + internalCaFns + }); + await secretRotationV2QueueServiceFactory({ secretRotationV2Service, secretRotationV2DAL, @@ -1847,6 +1865,7 @@ export const registerRoutes = async ( pkiAlert: pkiAlertService, pkiCollection: pkiCollectionService, pkiSubscriber: pkiSubscriberService, + pkiTemplate: pkiTemplateService, secretScanning: secretScanningService, license: licenseService, trustedIp: trustedIpService, diff --git a/backend/src/server/routes/v2/index.ts b/backend/src/server/routes/v2/index.ts index fb055877d..93c422d15 100644 --- a/backend/src/server/routes/v2/index.ts +++ b/backend/src/server/routes/v2/index.ts @@ -5,6 +5,7 @@ import { registerIdentityProjectRouter } from "./identity-project-router"; import { registerMfaRouter } from "./mfa-router"; import { registerOrgRouter } from "./organization-router"; import { registerPasswordRouter } from "./password-router"; +import { registerPkiTemplatesRouter } from "./pki-templates-router"; import { registerProjectMembershipRouter } from "./project-membership-router"; import { registerProjectRouter } from "./project-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(registerServiceTokenRouter, { prefix: "/service-token" }); 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( async (orgRouter) => { await orgRouter.register(registerOrgRouter); diff --git a/backend/src/server/routes/v2/pki-templates-router.ts b/backend/src/server/routes/v2/pki-templates-router.ts new file mode 100644 index 000000000..a085aecdb --- /dev/null +++ b/backend/src/server/routes/v2/pki-templates-router.ts @@ -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(), + caId: z.string(), + 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(), + caId: z.string(), + 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) + }), + 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; + } + }); +}; diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts index 457863121..643a9e127 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts @@ -1,8 +1,10 @@ +/* eslint-disable no-bitwise */ import * as x509 from "@peculiar/x509"; import { KeyObject } from "crypto"; +import RE2 from "re2"; 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 { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; @@ -31,6 +33,7 @@ import { keyAlgorithmToAlgCfg } from "../certificate-authority-fns"; import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal"; +import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types"; type TInternalCertificateAuthorityFnsDeps = { certificateAuthorityDAL: Pick; @@ -257,7 +260,274 @@ export const InternalCertificateAuthorityFns = ({ }; }; + const issueCertificateWithTemplate = async ( + ca: Awaited>, + 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: extendedKeyUsages as CertExtendedKeyUsage[], + 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 { - issueCertificate + issueCertificate, + issueCertificateWithTemplate }; }; diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts index 8c16ef3c2..083117241 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts @@ -1,5 +1,5 @@ /* eslint-disable no-bitwise */ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import slugify from "@sindresorhus/slugify"; import crypto, { KeyObject } from "crypto"; @@ -16,6 +16,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { ProjectPermissionActions, ProjectPermissionCertificateActions, + ProjectPermissionPkiTemplateActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; @@ -1952,15 +1953,15 @@ export const internalCertificateAuthorityServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateTemplates - ); - const certificateTemplates = await certificateTemplateDAL.find({ caId }); return { - certificateTemplates, + certificateTemplates: certificateTemplates.filter((el) => + permission.can( + ProjectPermissionPkiTemplateActions.Read, + subject(ProjectPermissionSub.CertificateTemplates, { name: el.name }) + ) + ), ca: expandInternalCa(ca) }; }; diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts index f8ea82a59..fadd7b88d 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts @@ -221,3 +221,13 @@ export type TOrderCertificateForSubscriberDTO = { subscriberId: string; caType: CaType; }; + +export type TIssueCertWithTemplateDTO = { + commonName: string; + altNames: string; + ttl: string; + notBefore?: string; + notAfter?: string; + keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; +}; diff --git a/backend/src/services/certificate-template/certificate-template-schema.ts b/backend/src/services/certificate-template/certificate-template-schema.ts index 7a87daddf..6ab39af7d 100644 --- a/backend/src/services/certificate-template/certificate-template-schema.ts +++ b/backend/src/services/certificate-template/certificate-template-schema.ts @@ -18,3 +18,20 @@ export const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({ 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() + }) +); diff --git a/backend/src/services/certificate-template/certificate-template-service.ts b/backend/src/services/certificate-template/certificate-template-service.ts index 04bf76f5c..be1200503 100644 --- a/backend/src/services/certificate-template/certificate-template-service.ts +++ b/backend/src/services/certificate-template/certificate-template-service.ts @@ -1,11 +1,14 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import bcrypt from "bcrypt"; import { ActionProjectType, TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-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 { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -78,8 +81,8 @@ export const certificateTemplateServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - ProjectPermissionSub.CertificateTemplates + ProjectPermissionPkiTemplateActions.Create, + subject(ProjectPermissionSub.CertificateTemplates, { name }) ); return certificateTemplateDAL.transaction(async (tx) => { @@ -140,8 +143,8 @@ export const certificateTemplateServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - ProjectPermissionSub.CertificateTemplates + ProjectPermissionPkiTemplateActions.Edit, + subject(ProjectPermissionSub.CertificateTemplates, { name: certTemplate.name }) ); 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) => { await certificateTemplateDAL.updateById( certTemplate.id, @@ -198,8 +208,8 @@ export const certificateTemplateServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - ProjectPermissionSub.CertificateTemplates + ProjectPermissionPkiTemplateActions.Delete, + subject(ProjectPermissionSub.CertificateTemplates, { name: certTemplate.name }) ); await certificateTemplateDAL.deleteById(certTemplate.id); @@ -225,8 +235,8 @@ export const certificateTemplateServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateTemplates + ProjectPermissionPkiTemplateActions.Read, + subject(ProjectPermissionSub.CertificateTemplates, { name: certTemplate.name }) ); return certTemplate; @@ -267,8 +277,8 @@ export const certificateTemplateServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - ProjectPermissionSub.CertificateTemplates + ProjectPermissionPkiTemplateActions.Edit, + subject(ProjectPermissionSub.CertificateTemplates, { name: certTemplate.name }) ); const appCfg = getConfig(); @@ -350,8 +360,8 @@ export const certificateTemplateServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - ProjectPermissionSub.CertificateTemplates + ProjectPermissionPkiTemplateActions.Edit, + subject(ProjectPermissionSub.CertificateTemplates, { name: certTemplate.name }) ); const originalCaEstConfig = await certificateTemplateEstConfigDAL.findOne({ @@ -430,8 +440,8 @@ export const certificateTemplateServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - ProjectPermissionSub.CertificateTemplates + ProjectPermissionPkiTemplateActions.Edit, + subject(ProjectPermissionSub.CertificateTemplates, { name: certTemplate.name }) ); } diff --git a/backend/src/services/pki-templates/pki-templates-dal.ts b/backend/src/services/pki-templates/pki-templates-dal.ts new file mode 100644 index 000000000..45c632d70 --- /dev/null +++ b/backend/src/services/pki-templates/pki-templates-dal.ts @@ -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; + +export const pkiTemplatesDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.CertificateTemplate); + + const findOne = async ( + filter: Partial, + 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 & { projectId: string }, + { + offset, + limit, + sort, + count, + tx, + countDistinct + }: TFindOpt = {} + ) => { + 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; + 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 }; +}; diff --git a/backend/src/services/pki-templates/pki-templates-service.ts b/backend/src/services/pki-templates/pki-templates-service.ts new file mode 100644 index 000000000..96ffa0183 --- /dev/null +++ b/backend/src/services/pki-templates/pki-templates-service.ts @@ -0,0 +1,624 @@ +/* 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; + certificateAuthorityDAL: Pick< + TCertificateAuthorityDALFactory, + "findByIdWithAssociatedCa" | "findById" | "transaction" | "create" | "updateById" | "findWithAssociatedCa" + >; + internalCaFns: ReturnType; + kmsService: Pick; + certificateAuthorityCertDAL: Pick; + certificateAuthoritySecretDAL: Pick; + certificateAuthorityCrlDAL: Pick; + certificateDAL: Pick< + TCertificateDALFactory, + "create" | "transaction" | "countCertificatesForPkiSubscriber" | "findLatestActiveCertForSubscriber" | "find" + >; + certificateSecretDAL: Pick; + certificateBodyDAL: Pick; + projectDAL: Pick; +}; + +export type TPkiTemplatesServiceFactory = ReturnType; + +export const pkiTemplatesServiceFactory = ({ + pkiTemplatesDAL, + permissionService, + internalCaFns, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + certificateAuthoritySecretDAL, + certificateAuthorityCrlDAL, + certificateDAL, + certificateBodyDAL, + kmsService, + projectDAL +}: TPkiTemplatesServiceFactoryDep) => { + const createTemplate = async ({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + caId, + commonName, + extendedKeyUsages, + keyUsages, + name, + subjectAlternativeName, + ttl + }: TCreatePkiTemplateDTO) => { + const ca = await certificateAuthorityDAL.findById(caId); + if (!ca) { + throw new NotFoundError({ + message: `CA with ID ${caId} 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 newTemplate = await pkiTemplatesDAL.create({ + caId, + name, + commonName, + subjectAlternativeName, + ttl, + keyUsages, + extendedKeyUsages + }); + return newTemplate; + }; + + const updateTemplate = async ({ + templateName, + actor, + actorId, + actorAuthMethod, + actorOrgId, + caId, + 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 }) + ); + + if (caId) { + const ca = await certificateAuthorityDAL.findById(caId); + if (!ca || ca.projectId !== certTemplate.projectId) { + throw new NotFoundError({ + message: `CA with ID ${caId} not found` + }); + } + } + + if (name) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiTemplateActions.Edit, + subject(ProjectPermissionSub.CertificateTemplates, { name }) + ); + } + + 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 + }; +}; diff --git a/backend/src/services/pki-templates/pki-templates-types.ts b/backend/src/services/pki-templates/pki-templates-types.ts new file mode 100644 index 000000000..72d245de1 --- /dev/null +++ b/backend/src/services/pki-templates/pki-templates-types.ts @@ -0,0 +1,53 @@ +import { TProjectPermission } from "@app/lib/types"; +import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types"; + +export type TCreatePkiTemplateDTO = { + caId: string; + name: string; + commonName: string; + subjectAlternativeName: string; + ttl: string; + keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; +} & TProjectPermission; + +export type TUpdatePkiTemplateDTO = { + templateName: string; + caId?: 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; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index d042ee32a..46f609777 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -17,6 +17,7 @@ import { ProjectPermissionActions, ProjectPermissionCertificateActions, ProjectPermissionPkiSubscriberActions, + ProjectPermissionPkiTemplateActions, ProjectPermissionSecretActions, ProjectPermissionSshHostActions, ProjectPermissionSub @@ -1131,15 +1132,15 @@ export const projectServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateTemplates - ); - const certificateTemplates = await certificateTemplateDAL.getCertTemplatesByProjectId(projectId); return { - certificateTemplates + certificateTemplates: certificateTemplates.filter((el) => + permission.can( + ProjectPermissionPkiTemplateActions.Read, + subject(ProjectPermissionSub.CertificateTemplates, { name: el.name }) + ) + ) }; }; From 3a0e2bf88b3b4759aee70d6b5e4154164bed7272 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 30 May 2025 14:41:01 +0530 Subject: [PATCH 2/9] feat: completed frontend changes for new pki templates --- .../ProjectPermissionContext/index.tsx | 1 + .../context/ProjectPermissionContext/types.ts | 22 +- frontend/src/context/index.tsx | 1 + .../hooks/api/certificateTemplates/index.tsx | 5 +- .../api/certificateTemplates/mutations.tsx | 55 +++ .../api/certificateTemplates/queries.tsx | 35 +- .../hooks/api/certificateTemplates/types.ts | 54 +++ .../layouts/ProjectLayout/ProjectLayout.tsx | 14 + .../CertificateTemplatesSection.tsx | 8 +- .../components/CertificateTemplatesTable.tsx | 10 +- .../PkiTemplateListPage.tsx | 267 ++++++++++++ .../components/PkiTemplateForm.tsx | 407 ++++++++++++++++++ .../PkiTemplateListPage/route.tsx | 9 + .../PkiTemplatePermissionConditions.tsx | 173 ++++++++ .../ProjectRoleModifySection.utils.tsx | 60 ++- .../components/RolePermissionsSection.tsx | 5 + frontend/src/routeTree.gen.ts | 76 ++++ frontend/src/routes.ts | 1 + 18 files changed, 1188 insertions(+), 15 deletions(-) create mode 100644 frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx create mode 100644 frontend/src/pages/cert-manager/PkiTemplateListPage/components/PkiTemplateForm.tsx create mode 100644 frontend/src/pages/cert-manager/PkiTemplateListPage/route.tsx create mode 100644 frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiTemplatePermissionConditions.tsx diff --git a/frontend/src/context/ProjectPermissionContext/index.tsx b/frontend/src/context/ProjectPermissionContext/index.tsx index d7b7334ea..f564ac74e 100644 --- a/frontend/src/context/ProjectPermissionContext/index.tsx +++ b/frontend/src/context/ProjectPermissionContext/index.tsx @@ -10,6 +10,7 @@ export { ProjectPermissionKmipActions, ProjectPermissionMemberActions, ProjectPermissionPkiSubscriberActions, + ProjectPermissionPkiTemplateActions, ProjectPermissionSshHostActions, ProjectPermissionSub } from "./types"; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index a640f6eaa..169338039 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -104,6 +104,15 @@ export enum ProjectPermissionPkiSubscriberActions { ListCerts = "list-certs" } +export enum ProjectPermissionPkiTemplateActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + IssueCert = "issue-cert", + ListCerts = "list-certs" +} + export enum ProjectPermissionSecretRotationActions { Read = "read", ReadGeneratedCredentials = "read-generated-credentials", @@ -238,6 +247,11 @@ export type PkiSubscriberSubjectFields = { name: string; }; +export type PkiTemplateSubjectFields = { + name: string; + // (dangtony98): consider adding [commonName] as a subject field in the future +}; + export type ProjectPermissionSet = | [ ProjectPermissionSecretActions, @@ -295,7 +309,13 @@ export type ProjectPermissionSet = ] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates] - | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] + | [ + ProjectPermissionPkiTemplateActions, + ( + | ProjectPermissionSub.CertificateTemplates + | (ForcedSubject & PkiTemplateSubjectFields) + ) + ] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx index 3641e8a3c..833956d77 100644 --- a/frontend/src/context/index.tsx +++ b/frontend/src/context/index.tsx @@ -19,6 +19,7 @@ export { ProjectPermissionKmipActions, ProjectPermissionMemberActions, ProjectPermissionPkiSubscriberActions, + ProjectPermissionPkiTemplateActions, ProjectPermissionSshHostActions, ProjectPermissionSub, useProjectPermission diff --git a/frontend/src/hooks/api/certificateTemplates/index.tsx b/frontend/src/hooks/api/certificateTemplates/index.tsx index 61dfb35a2..a48248a6b 100644 --- a/frontend/src/hooks/api/certificateTemplates/index.tsx +++ b/frontend/src/hooks/api/certificateTemplates/index.tsx @@ -1,8 +1,11 @@ export { useCreateCertTemplate, + useCreateCertTemplateV2, useCreateEstConfig, useDeleteCertTemplate, + useDeleteCertTemplateV2, useUpdateCertTemplate, + useUpdateCertTemplateV2, useUpdateEstConfig } from "./mutations"; -export { useGetCertTemplate, useGetEstConfig } from "./queries"; +export { useGetCertTemplate, useGetEstConfig, useListCertificateTemplates } from "./queries"; diff --git a/frontend/src/hooks/api/certificateTemplates/mutations.tsx b/frontend/src/hooks/api/certificateTemplates/mutations.tsx index 55bbcd417..d1069e7e6 100644 --- a/frontend/src/hooks/api/certificateTemplates/mutations.tsx +++ b/frontend/src/hooks/api/certificateTemplates/mutations.tsx @@ -8,9 +8,12 @@ import { certTemplateKeys } from "./queries"; import { TCertificateTemplate, TCreateCertificateTemplateDTO, + TCreateCertificateTemplateV2DTO, TCreateEstConfigDTO, TDeleteCertificateTemplateDTO, + TDeleteCertificateTemplateV2DTO, TUpdateCertificateTemplateDTO, + TUpdateCertificateTemplateV2DTO, TUpdateEstConfigDTO } from "./types"; @@ -73,6 +76,58 @@ export const useDeleteCertTemplate = () => { }); }; +export const useCreateCertTemplateV2 = () => { + const queryClient = useQueryClient(); + return useMutation({ + 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({ + 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({ + 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 = () => { const queryClient = useQueryClient(); return useMutation({ diff --git a/frontend/src/hooks/api/certificateTemplates/queries.tsx b/frontend/src/hooks/api/certificateTemplates/queries.tsx index 7ee5bbd30..435345ad9 100644 --- a/frontend/src/hooks/api/certificateTemplates/queries.tsx +++ b/frontend/src/hooks/api/certificateTemplates/queries.tsx @@ -2,10 +2,20 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { TCertificateTemplate, TEstConfig } from "./types"; +import { + TCertificateTemplate, + TCertificateTemplateV2, + TEstConfig, + TListCertificateTemplatesDTO +} from "./types"; export const certTemplateKeys = { 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"] }; @@ -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) => { return useQuery({ queryKey: certTemplateKeys.getEstConfig(certificateTemplateId), diff --git a/frontend/src/hooks/api/certificateTemplates/types.ts b/frontend/src/hooks/api/certificateTemplates/types.ts index 14367a280..3e3e728f8 100644 --- a/frontend/src/hooks/api/certificateTemplates/types.ts +++ b/frontend/src/hooks/api/certificateTemplates/types.ts @@ -14,6 +14,26 @@ export type TCertificateTemplate = { 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 = { caId: string; pkiCollectionId?: string; @@ -44,6 +64,34 @@ export type TDeleteCertificateTemplateDTO = { projectId: string; }; +export type TCreateCertificateTemplateV2DTO = { + caId: string; + name: string; + commonName: string; + subjectAlternativeName: string; + ttl: string; + projectId: string; + keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; +}; + +export type TUpdateCertificateTemplateV2DTO = { + templateName: string; + caId?: 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 = { certificateTemplateId: string; caChain?: string; @@ -67,3 +115,9 @@ export type TEstConfig = { isEnabled: boolean; disableBootstrapCertValidation: boolean; }; + +export type TListCertificateTemplatesDTO = { + limit?: number; + offset?: number; + projectId: string; +}; diff --git a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx index 35534d4a2..ea331f866 100644 --- a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx +++ b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx @@ -117,6 +117,20 @@ export const ProjectLayout = () => { )} + + {({ isActive }) => ( + + Certificate Templates + + )} + {

Certificate Templates

{(isAllowed) => ( diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplatesTable.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplatesTable.tsx index 64b829844..e7d137807 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplatesTable.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplatesTable.tsx @@ -19,7 +19,11 @@ import { Tooltip, Tr } 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 { UsePopUpState } from "@app/hooks/usePopUp"; @@ -79,7 +83,7 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => { Manage Policies {(isAllowed) => ( @@ -105,7 +109,7 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => { )} {(isAllowed) => ( diff --git a/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx b/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx new file mode 100644 index 000000000..7bbb08008 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx @@ -0,0 +1,267 @@ +import { useState } from "react"; +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; +import { + faArrowUpRightFromSquare, + 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 ( + <> + + {t("common.head-title", { title: "PKI Subscribers" })} + +
+
+
+ +
+
+
+

Templates

+
+ + + Documentation{" "} + + + + + {(isAllowed) => ( + + )} + +
+
+ + + + + + + + + + + {isPending && } + {!isPending && + data?.certificateTemplates?.map((template) => { + return ( + + + + + + + ); + })} + {!isPending && !data?.certificateTemplates?.length && ( + + + + )} + +
NameCALast Updated At +
{template.name} + {template.ca.name} + {format(new Date(template.updatedAt), "yyyy-MM-dd | HH:mm:ss")} + + +
+ + + +
+
+ + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("certificateTemplate", template); + }} + disabled={!isAllowed} + icon={} + > + Edit Template + + )} + + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("deleteTemplate", template); + }} + disabled={!isAllowed} + icon={} + > + Delete Template + + )} + + +
+
+ +
+ {!isPending && data?.totalCount !== undefined && data.totalCount >= PER_PAGE_INIT && ( + setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )} +
+ handlePopUpToggle("deleteTemplate", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => onRemovePkiSubscriberSubmit()} + /> +
+
+
+ handlePopUpToggle("certificateTemplate", isOpen)} + > + + handlePopUpToggle("certificateTemplate", isOpen)} + /> + + +
+ + ); +}; diff --git a/frontend/src/pages/cert-manager/PkiTemplateListPage/components/PkiTemplateForm.tsx b/frontend/src/pages/cert-manager/PkiTemplateListPage/components/PkiTemplateForm.tsx new file mode 100644 index 000000000..0a78ddac2 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiTemplateListPage/components/PkiTemplateForm.tsx @@ -0,0 +1,407 @@ +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; + +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({ + 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, + caId: ca.id, + 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, + caId: ca.id, + 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 ( +
+ {certTemplate && ( + + + + )} + ( + + + + )} + /> + ( + + option.id} + getOptionLabel={(option) => option.name} + /> + + )} + control={control} + name="ca" + /> + ( + + + This field accepts limited regular expressions: spaces, *, ., @, -, \ (for + escaping), and alphanumeric characters only + + } + > + + + } + /> +
+ } + isError={Boolean(error)} + errorText={error?.message} + isRequired + > + + + )} + /> + ( + + + This field accepts limited regular expressions: spaces, *, ., @, -, \ (for + escaping), and alphanumeric characters only + + } + > + + + } + /> +
+ } + isError={Boolean(error)} + errorText={error?.message} + isRequired + > + + + )} + /> + ( + + + + )} + /> + + + +
Key Usage
+
+ + { + return ( + +
+ {KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} +
+
+ ); + }} + /> + { + return ( + +
+ {EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} +
+
+ ); + }} + /> +
+
+
+
+ + +
+ + ); +}; diff --git a/frontend/src/pages/cert-manager/PkiTemplateListPage/route.tsx b/frontend/src/pages/cert-manager/PkiTemplateListPage/route.tsx new file mode 100644 index 000000000..5647c45f5 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiTemplateListPage/route.tsx @@ -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 +}); diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiTemplatePermissionConditions.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiTemplatePermissionConditions.tsx new file mode 100644 index 000000000..b581b7bea --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiTemplatePermissionConditions.tsx @@ -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(); + + const permissionSubject = ProjectPermissionSub.CertificateTemplates; + const items = useFieldArray({ + control, + name: `permissions.${permissionSubject}.${position}.conditions` + }); + + return ( +
+

Conditions

+

+ Conditions determine when a policy will be applied (always if no conditions are present). +

+

+ All conditions must evaluate to true for the policy to take effect. +

+
+ {items.fields.map((el, index) => { + const condition = + (watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as { + lhs: string; + rhs: string; + operator: string; + }) || {}; + + return ( +
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> + + + +
+
+ ( + + + + )} + /> +
+
+ items.remove(index)} + > + + +
+
+ ); + })} +
+ {errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && ( +
+ + {errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message} +
+ )} +
+ +
+
+ ); +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index a54c844ad..09a2638cc 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -18,6 +18,7 @@ import { ProjectPermissionKmipActions, ProjectPermissionMemberActions, ProjectPermissionPkiSubscriberActions, + ProjectPermissionPkiTemplateActions, ProjectPermissionSecretActions, ProjectPermissionSecretRotationActions, ProjectPermissionSecretSyncActions, @@ -148,6 +149,15 @@ const PkiSubscriberPolicyActionSchema = z.object({ [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({ read: z.boolean().optional(), create: z.boolean().optional() @@ -255,7 +265,12 @@ export const projectRoleFormSchema = z.object({ .default([]), [ProjectPermissionSub.PkiAlerts]: 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( [] ), @@ -295,6 +310,7 @@ type TConditionalFields = | ProjectPermissionSub.SecretImports | ProjectPermissionSub.DynamicSecrets | ProjectPermissionSub.PkiSubscribers + | ProjectPermissionSub.CertificateTemplates | ProjectPermissionSub.SshHosts | ProjectPermissionSub.SecretRotation | ProjectPermissionSub.Identity; @@ -309,7 +325,8 @@ export const isConditionalSubjects = ( subject === ProjectPermissionSub.Identity || subject === ProjectPermissionSub.SshHosts || subject === ProjectPermissionSub.SecretRotation || - subject === ProjectPermissionSub.PkiSubscribers; + subject === ProjectPermissionSub.PkiSubscribers || + subject === ProjectPermissionSub.CertificateTemplates; const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => { const formConditions: z.infer = []; @@ -408,7 +425,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { ProjectPermissionSub.CertificateAuthorities, ProjectPermissionSub.PkiAlerts, ProjectPermissionSub.PkiCollections, - ProjectPermissionSub.CertificateTemplates, ProjectPermissionSub.Tags, ProjectPermissionSub.SecretRotation, ProjectPermissionSub.Kms, @@ -781,6 +797,34 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], 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]: { title: "Certificate Templates", actions: [ - { label: "Read", value: "read" }, - { label: "Create", value: "create" }, - { label: "Modify", value: "edit" }, - { label: "Remove", value: "delete" } + { label: "Read", value: ProjectPermissionPkiTemplateActions.Read }, + { label: "Create", value: ProjectPermissionPkiTemplateActions.Create }, + { label: "Modify", value: ProjectPermissionPkiTemplateActions.Edit }, + { label: "Remove", value: ProjectPermissionPkiTemplateActions.Delete }, + { label: "Issue Certificates", value: ProjectPermissionPkiTemplateActions.IssueCert }, + { label: "List Certificates", value: ProjectPermissionPkiTemplateActions.ListCerts } ] }, [ProjectPermissionSub.SshCertificateAuthorities]: { diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx index 593b40e93..a1bbe3d54 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx @@ -23,6 +23,7 @@ import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies"; import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions"; import { PermissionEmptyState } from "./PermissionEmptyState"; import { PkiSubscriberPermissionConditions } from "./PkiSubscriberPermissionConditions"; +import { PkiTemplatePermissionConditions } from "./PkiTemplatePermissionConditions"; import { formRolePermission2API, isConditionalSubjects, @@ -63,6 +64,10 @@ export const renderConditionalComponents = ( return ; } + if (subject === ProjectPermissionSub.CertificateTemplates) { + return ; + } + return ; } diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index bc19deb6c..72b28878f 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -123,6 +123,7 @@ import { Route as certManagerPkiSubscriberDetailsByIDPageRouteImport } from './p import { Route as certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route' import { Route as secretManagerIntegrationsListPageRouteImport } from './pages/secret-manager/IntegrationsListPage/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 secretManagerIntegrationsWindmillAuthorizePageRouteImport } from './pages/secret-manager/integrations/WindmillAuthorizePage/route' import { Route as secretManagerIntegrationsVercelConfigurePageRouteImport } from './pages/secret-manager/integrations/VercelConfigurePage/route' @@ -257,6 +258,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayout createFileRoute( '/_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 @@ -870,6 +875,15 @@ const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayout } as any, ) +const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRoute = + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesImport.update( + { + id: '/certificate-templates', + path: '/certificate-templates', + getParentRoute: () => certManagerLayoutRoute, + } as any, + ) + const projectAccessControlPageRouteCertManagerRoute = projectAccessControlPageRouteCertManagerImport.update({ id: '/access-management', @@ -1156,6 +1170,14 @@ const certManagerPkiSubscribersPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute, } as any) +const certManagerPkiTemplateListPageRouteRoute = + certManagerPkiTemplateListPageRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRoute, + } as any) + const secretManagerIntegrationsWindmillConfigurePageRouteRoute = secretManagerIntegrationsWindmillConfigurePageRouteImport.update({ id: '/windmill/create', @@ -2391,6 +2413,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteCertManagerImport 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': { id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers' path: '/subscribers' @@ -2489,6 +2518,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteSshImport 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/': { id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/' path: '/' @@ -3360,6 +3396,21 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteWithChildren = AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren, ) +interface AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteChildren { + certManagerPkiTemplateListPageRouteRoute: typeof certManagerPkiTemplateListPageRouteRoute +} + +const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteChildren = + { + certManagerPkiTemplateListPageRouteRoute: + certManagerPkiTemplateListPageRouteRoute, + } + +const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren = + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRoute._addFileChildren( + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteChildren, + ) + interface AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteChildren { certManagerPkiSubscribersPageRouteRoute: typeof certManagerPkiSubscribersPageRouteRoute certManagerPkiSubscriberDetailsByIDPageRouteRoute: typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute @@ -3384,6 +3435,7 @@ interface certManagerLayoutRouteChildren { certManagerCertificatesPageRouteRoute: typeof certManagerCertificatesPageRouteRoute certManagerSettingsPageRouteRoute: typeof certManagerSettingsPageRouteRoute projectAccessControlPageRouteCertManagerRoute: typeof projectAccessControlPageRouteCertManagerRoute + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren certManagerCertAuthDetailsByIDPageRouteRoute: typeof certManagerCertAuthDetailsByIDPageRouteRoute projectIdentityDetailsByIDPageRouteCertManagerRoute: typeof projectIdentityDetailsByIDPageRouteCertManagerRoute @@ -3400,6 +3452,8 @@ const certManagerLayoutRouteChildren: certManagerLayoutRouteChildren = { certManagerSettingsPageRouteRoute: certManagerSettingsPageRouteRoute, projectAccessControlPageRouteCertManagerRoute: projectAccessControlPageRouteCertManagerRoute, + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRoute: + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren, AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute: AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren, certManagerCertAuthDetailsByIDPageRouteRoute: @@ -4089,6 +4143,7 @@ export interface FileRoutesByFullPath { '/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute '/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute '/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute + '/cert-manager/$projectId/certificate-templates': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren '/cert-manager/$projectId/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren '/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute '/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/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren '/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute + '/cert-manager/$projectId/certificate-templates/': typeof certManagerPkiTemplateListPageRouteRoute '/cert-manager/$projectId/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute '/secret-manager/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute '/cert-manager/$projectId/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute @@ -4288,6 +4344,7 @@ export interface FileRoutesByTo { '/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute '/secret-manager/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute '/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute + '/cert-manager/$projectId/certificate-templates': typeof certManagerPkiTemplateListPageRouteRoute '/cert-manager/$projectId/subscribers': typeof certManagerPkiSubscribersPageRouteRoute '/secret-manager/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute '/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/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/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/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute '/_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/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren '/_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/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 @@ -4676,6 +4735,7 @@ export interface FileRouteTypes { | '/ssh/$projectId/overview' | '/ssh/$projectId/settings' | '/cert-manager/$projectId/access-management' + | '/cert-manager/$projectId/certificate-templates' | '/cert-manager/$projectId/subscribers' | '/integrations/azure-app-configuration/oauth2/callback' | '/integrations/azure-key-vault/oauth2/callback' @@ -4690,6 +4750,7 @@ export interface FileRouteTypes { | '/secret-manager/$projectId/access-management' | '/secret-manager/$projectId/integrations' | '/ssh/$projectId/access-management' + | '/cert-manager/$projectId/certificate-templates/' | '/cert-manager/$projectId/subscribers/' | '/secret-manager/$projectId/integrations/' | '/cert-manager/$projectId/ca/$caName' @@ -4874,6 +4935,7 @@ export interface FileRouteTypes { | '/kms/$projectId/access-management' | '/secret-manager/$projectId/access-management' | '/ssh/$projectId/access-management' + | '/cert-manager/$projectId/certificate-templates' | '/cert-manager/$projectId/subscribers' | '/secret-manager/$projectId/integrations' | '/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/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/certificate-templates' | '/_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-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/integrations' | '/_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/secret-manager/$projectId/_secret-manager-layout/integrations/' | '/_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/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/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/ca/$caName", "/_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", "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": { "filePath": "", "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", "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/": { "filePath": "cert-manager/PkiSubscribersPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers" diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 7f2a250e7..8f7353e44 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -293,6 +293,7 @@ const certManagerRoutes = route("/cert-manager/$projectId", [ index("cert-manager/PkiSubscribersPage/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("/certificate-authorities", "cert-manager/CertificateAuthoritiesPage/route.tsx"), route("/alerting", "cert-manager/AlertingPage/route.tsx"), From 3362ec29cd4cfa5b2e70b6d4963d7c7610767cd8 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 30 May 2025 19:51:13 +0530 Subject: [PATCH 3/9] feat: updated doc for k8s issuer --- .../documentation/platform/pki/pki-issuer.mdx | 122 ++++++++++++------ .../CertAuthDetailsByIDPage.tsx | 2 - .../PkiTemplateListPage.tsx | 10 -- 3 files changed, 84 insertions(+), 50 deletions(-) diff --git a/docs/documentation/platform/pki/pki-issuer.mdx b/docs/documentation/platform/pki/pki-issuer.mdx index c02e477c6..9f6d6595b 100644 --- a/docs/documentation/platform/pki/pki-issuer.mdx +++ b/docs/documentation/platform/pki/pki-issuer.mdx @@ -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. 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. -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. -7. Consuming the issued certificate across your Kubernetes resources from the specified Kubernetes `Secret`. +6. Create an the approver policy to accept certificate request. +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 -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. - + 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. - + 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 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 kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.3/cert-manager.yaml ``` Install the Infisical PKI Issuer controller into your Kubernetes cluster by running the following command: - + ```bash 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: clientSecret: ``` - + ```bash kubectl apply -f secret-issuer.yaml ``` @@ -84,7 +85,7 @@ In the following steps, we explore how to install the Infisical PKI Issuer using - 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. ```yaml infisical-issuer.yaml @@ -95,8 +96,8 @@ In the following steps, we explore how to install the Infisical PKI Issuer using namespace: spec: url: "https://app.infisical.com" # the URL of your Infisical instance - caId: # the ID of the CA you want to use to issue certificates - certificateTemplateId: # the ID of the certificate template you want to use to issue certificates against + projectId: # the ID of the project you want to use to issue certificates + certificateTemplateName: # the name of the certificate template you want to use to issue certificates against authentication: universalAuth: clientId: # 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" key: "clientSecret" ``` - + ``` kubectl apply -f infisical-issuer.yaml ``` - - - 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. - - + You can check that the issuer was created successfully by running the following command: ```bash @@ -128,16 +120,60 @@ In the following steps, we explore how to install the Infisical PKI Issuer using NAME AGE issuer-infisical 21h ``` - + 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. 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/). + + 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/). + 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 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 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 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 kubectl get secret certificate-by-issuer -n ``` @@ -190,9 +226,9 @@ In the following steps, we explore how to install the Infisical PKI Issuer using NAME TYPE DATA AGE certificate-by-issuer kubernetes.io/tls 2 26h ``` - + We can `describe` the secret to get more information about it: - + ```bash 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 Namespace: default 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/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-kind: Issuer cert-manager.io/issuer-name: issuer-infisical - cert-manager.io/uri-sans: + cert-manager.io/uri-sans: 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.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. - + We can decode the certificate and print it out using `openssl`: ```bash 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. + ## FAQ @@ -236,15 +273,24 @@ In the following steps, we explore how to install the Infisical PKI Issuer using 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). - + Currently, not all fields are supported by the Infisical PKI Issuer. + Yes. `cert-manager` will automatically renew certificates according to the `renewBefore` threshold of expiry as 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). + - \ No newline at end of file + + 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. + + diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx index e76720275..dd202f981 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx @@ -23,7 +23,6 @@ import { usePopUp } from "@app/hooks/usePopUp"; import { CaInstallCertModal } from "../CertificateAuthoritiesPage/components/CaInstallCertModal"; import { CaModal } from "../CertificateAuthoritiesPage/components/CaModal"; -import { CertificateTemplatesSection } from "../CertificatesPage/components/CertificateTemplatesSection"; import { CaCertificatesSection, CaCrlsSection, @@ -126,7 +125,6 @@ const Page = () => {
-
diff --git a/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx b/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx index 7bbb08008..7adf86bc7 100644 --- a/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx +++ b/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx @@ -2,7 +2,6 @@ import { useState } from "react"; import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; import { - faArrowUpRightFromSquare, faCertificate, faEllipsis, faPencil, @@ -107,15 +106,6 @@ export const PkiTemplateListPage = () => {

Templates

- - - Documentation{" "} - - - Date: Fri, 30 May 2025 20:02:13 +0530 Subject: [PATCH 4/9] feat: added slugification to old routes --- backend/src/server/routes/v1/certificate-template-router.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/server/routes/v1/certificate-template-router.ts b/backend/src/server/routes/v1/certificate-template-router.ts index b0c186206..17f564be4 100644 --- a/backend/src/server/routes/v1/certificate-template-router.ts +++ b/backend/src/server/routes/v1/certificate-template-router.ts @@ -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 { 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"; @@ -72,7 +73,7 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid body: z.object({ caId: z.string().describe(CERTIFICATE_TEMPLATES.CREATE.caId), pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.CREATE.pkiCollectionId), - name: z.string().min(1).describe(CERTIFICATE_TEMPLATES.CREATE.name), + name: slugSchema().describe(CERTIFICATE_TEMPLATES.CREATE.name), commonName: validateTemplateRegexField.describe(CERTIFICATE_TEMPLATES.CREATE.commonName), subjectAlternativeName: validateTemplateRegexField.describe( CERTIFICATE_TEMPLATES.CREATE.subjectAlternativeName @@ -141,7 +142,7 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid body: z.object({ caId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.caId), pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.pkiCollectionId), - name: z.string().min(1).optional().describe(CERTIFICATE_TEMPLATES.UPDATE.name), + name: slugSchema().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.name), commonName: validateTemplateRegexField.optional().describe(CERTIFICATE_TEMPLATES.UPDATE.commonName), subjectAlternativeName: validateTemplateRegexField .optional() From 86348eb434799d9b81b8ca74bc1c62b75b1dc2cf Mon Sep 17 00:00:00 2001 From: = Date: Fri, 30 May 2025 20:39:41 +0530 Subject: [PATCH 5/9] feat: completed reptile reviews --- backend/src/server/routes/v2/pki-templates-router.ts | 2 +- .../internal/internal-certificate-authority-fns.ts | 2 +- .../services/pki-templates/pki-templates-service.ts | 10 ++++++++++ docs/documentation/platform/pki/pki-issuer.mdx | 2 +- .../components/PkiTemplatePermissionConditions.tsx | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/backend/src/server/routes/v2/pki-templates-router.ts b/backend/src/server/routes/v2/pki-templates-router.ts index a085aecdb..b6b969706 100644 --- a/backend/src/server/routes/v2/pki-templates-router.ts +++ b/backend/src/server/routes/v2/pki-templates-router.ts @@ -281,7 +281,7 @@ export const registerPkiTemplatesRouter = async (server: FastifyZodProvider) => 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) + csr: z.string().trim().min(1).max(4096) }), response: { 200: z.object({ diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts index 643a9e127..def2e2bed 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts @@ -490,7 +490,7 @@ export const InternalCertificateAuthorityFns = ({ notBefore: notBeforeDate, notAfter: notAfterDate, keyUsages: selectedKeyUsages, - extendedKeyUsages: extendedKeyUsages as CertExtendedKeyUsage[], + extendedKeyUsages: selectedExtendedKeyUsages, projectId: ca.projectId, certificateTemplateId: certificateTemplate.id }, diff --git a/backend/src/services/pki-templates/pki-templates-service.ts b/backend/src/services/pki-templates/pki-templates-service.ts index 96ffa0183..e72a2198a 100644 --- a/backend/src/services/pki-templates/pki-templates-service.ts +++ b/backend/src/services/pki-templates/pki-templates-service.ts @@ -120,6 +120,11 @@ export const pkiTemplatesServiceFactory = ({ 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, name, @@ -182,6 +187,11 @@ export const pkiTemplatesServiceFactory = ({ 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, { diff --git a/docs/documentation/platform/pki/pki-issuer.mdx b/docs/documentation/platform/pki/pki-issuer.mdx index 9f6d6595b..42d40114b 100644 --- a/docs/documentation/platform/pki/pki-issuer.mdx +++ b/docs/documentation/platform/pki/pki-issuer.mdx @@ -21,7 +21,7 @@ A typical workflow for using the Infisical PKI Issuer to issue certificates for 3. Installing `cert-manager` 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. -6. Create an the approver policy to accept certificate request. +6. Create the approver policy to accept certificate request. 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`. diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiTemplatePermissionConditions.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiTemplatePermissionConditions.tsx index b581b7bea..2ca4e2d14 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiTemplatePermissionConditions.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiTemplatePermissionConditions.tsx @@ -132,7 +132,7 @@ export const PkiTemplatePermissionConditions = ({ position = 0, isDisabled }: Pr
items.remove(index)} From 5e3d4edec9270b369b10b17572d015b335a1de85 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 30 May 2025 20:49:51 +0530 Subject: [PATCH 6/9] feat: added new lottie --- frontend/public/lotties/pki-template.json | 3098 +++++++++++++++++ .../layouts/ProjectLayout/ProjectLayout.tsx | 6 +- 2 files changed, 3103 insertions(+), 1 deletion(-) create mode 100644 frontend/public/lotties/pki-template.json diff --git a/frontend/public/lotties/pki-template.json b/frontend/public/lotties/pki-template.json new file mode 100644 index 000000000..0001f3d07 --- /dev/null +++ b/frontend/public/lotties/pki-template.json @@ -0,0 +1,3098 @@ +{ + "v": "5.7.5", + "fr": 100, + "ip": 0, + "op": 250, + "w": 512, + "h": 532, + "nm": "Comp 1", + "ddd": 0, + "metadata": {}, + "assets": [], + "layers": [ + { + "ddd": 0, + "ind": 12345679, + "ty": 4, + "nm": "Group Layer 8", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [373.76, 495.53049180327866, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [52.459016393442624, 52.459016393442624, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [220.741, 37.184], + [225.501, 35.896], + [228.749, 32.36800000000001], + [229.981, 27.216000000000008], + [228.749, 22.12], + [225.501, 18.592], + [220.741, 17.304], + [215.981, 18.592], + [212.677, 22.12], + [211.501, 27.216000000000008], + [212.677, 32.36800000000001], + [215.981, 35.896], + [220.741, 37.184], + [220.741, 37.184], + [220.741, 37.184] + ], + "i": [ + [0, 0], + [-1.380999999999972, 0.8586999999999989], + [-0.7839999999999918, 1.493299999999991], + [0, 1.903999999999996], + [0.8220000000000027, 1.493299999999991], + [1.382000000000062, 0.8586999999999989], + [1.79200000000003, 0], + [1.418999999999983, -0.8586999999999989], + [0.8220000000000027, -1.493300000000005], + [0, -1.904000000000011], + [-0.7839999999999918, -1.5307000000000102], + [-1.380999999999972, -0.8586999999999989], + [-1.754000000000019, 0], + [0, 0], + [0, 0] + ], + "o": [ + [1.79200000000003, 0], + [1.382000000000062, -0.8586999999999989], + [0.8220000000000027, -1.5307000000000102], + [0, -1.904000000000011], + [-0.7839999999999918, -1.493300000000005], + [-1.380999999999972, -0.8586999999999989], + [-1.754000000000019, 0], + [-1.380999999999972, 0.8586999999999989], + [-0.7839999999999918, 1.493299999999991], + [0, 1.903999999999996], + [0.8220000000000027, 1.493299999999991], + [1.418999999999983, 0.8586999999999989], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [221.357, 43.06400000000001], + [214.917, 41.608], + [210.49300000000005, 37.408], + [211.221, 36.232], + [211.221, 42.392], + [205.173, 42.392], + [205.173, 0], + [211.501, 0], + [211.501, 18.36800000000001], + [210.49300000000005, 16.912000000000006], + [214.973, 12.88], + [221.357, 11.424000000000007], + [229.085, 13.49600000000001], + [234.51700000000005, 19.152], + [236.533, 27.216000000000008], + [234.51700000000005, 35.28], + [229.141, 40.992], + [221.357, 43.06400000000001], + [221.357, 43.06400000000001], + [221.357, 43.06400000000001] + ], + "i": [ + [0, 0], + [1.942000000000007, 0.9706999999999937], + [1.045999999999935, 1.829300000000003], + [-0.2426666666666506, 0.3919999999999959], + [0, -2.053333333333327], + [2.015999999999963, 0], + [0, 14.13066666666667], + [-2.109333333333325, 0], + [0, -6.122666666666674], + [0.3360000000000127, 0.4853333333333296], + [-1.865999999999985, 0.9707000000000079], + [-2.38900000000001, 0], + [-2.277000000000044, -1.3813000000000102], + [-1.30600000000004, -2.389300000000006], + [0, -2.986699999999999], + [1.343999999999937, -2.389300000000006], + [2.27800000000002, -1.4187000000000012], + [2.912000000000035, 0], + [0, 0], + [0, 0] + ], + "o": [ + [-2.351999999999975, 0], + [-1.903999999999996, -0.9707000000000079], + [0.2426666666666506, -0.3919999999999959], + [0, 2.053333333333327], + [-2.015999999999963, 0], + [0, -14.13066666666667], + [2.109333333333325, 0], + [0, 6.122666666666667], + [-0.3360000000000127, -0.4853333333333296], + [1.120000000000005, -1.717300000000009], + [1.867000000000075, -0.9706999999999937], + [2.875, 0], + [2.314999999999941, 1.381299999999996], + [1.343999999999937, 2.389300000000006], + [0, 2.986699999999999], + [-1.30600000000004, 2.389300000000006], + [-2.27699999999993, 1.381299999999996], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [181.87, 43.06400000000001], + [176.438, 42], + [172.854, 38.976], + [171.566, 34.384], + [172.63, 29.960000000000008], + [176.046, 26.656000000000006], + [181.814, 24.752], + [192.342, 23.016000000000005], + [192.342, 28], + [183.046, 29.624], + [179.35, 31.248], + [178.174, 34.16], + [179.462, 37.016000000000005], + [182.878, 38.08], + [187.35799999999995, 36.96000000000001], + [190.38199999999995, 33.992], + [191.446, 29.792], + [191.446, 22.008], + [189.766, 18.36800000000001], + [185.398, 16.912000000000006], + [180.974, 18.256], + [178.23, 21.616], + [172.966, 18.98400000000001], + [175.71, 15.064000000000007], + [180.134, 12.376], + [185.566, 11.424000000000007], + [191.894, 12.768], + [196.206, 16.52], + [197.774, 22.008], + [197.774, 42.392], + [191.726, 42.392], + [191.726, 36.904], + [193.014, 37.072], + [190.27, 40.264], + [186.518, 42.336], + [181.87, 43.06400000000001], + [181.87, 43.06400000000001], + [181.87, 43.06400000000001] + ], + "i": [ + [0, 0], + [1.567999999999984, 0.7092999999999989], + [0.8589999999999236, 1.2693000000000012], + [0, 1.7547], + [-0.7089999999999463, 1.306699999999992], + [-1.530000000000086, 0.8960000000000008], + [-2.313999999999965, 0.3733000000000004], + [-3.509333333333302, 0.5786666666666633], + [0, -1.661333333333332], + [3.098666666666645, -0.541333333333327], + [0.7839999999999918, -0.784000000000006], + [0, -1.194699999999997], + [-0.8579999999999472, -0.7467000000000041], + [-1.381000000000085, 0], + [-1.268999999999892, 0.7466999999999899], + [-0.7089999999999463, 1.2319999999999993], + [0, 1.530699999999996], + [0, 2.594666666666669], + [1.120000000000005, 0.9332999999999885], + [1.829999999999927, 0], + [1.269999999999982, -0.8960000000000008], + [0.5979999999999563, -1.381299999999996], + [1.754666666666708, 0.8773333333333255], + [-1.269000000000005, 1.11999999999999], + [-1.680000000000064, 0.6346999999999952], + [-1.903999999999996, 0], + [-1.828999999999951, -0.8960000000000008], + [-1.008000000000038, -1.6053], + [0, -2.090699999999998], + [0, -6.794666666666672], + [2.015999999999963, 0], + [0, 1.829333333333338], + [-0.4293333333333749, -0.05599999999999739], + [1.120000000000005, -0.8959999999999866], + [1.418999999999983, -0.4852999999999952], + [1.717999999999961, 0], + [0, 0], + [0, 0] + ], + "o": [ + [-2.052999999999997, 0], + [-1.529999999999973, -0.7467000000000041], + [-0.8580000000000609, -1.306699999999992], + [0, -1.642700000000005], + [0.7469999999999573, -1.306700000000006], + [1.530999999999949, -0.8960000000000008], + [3.509333333333302, -0.5786666666666633], + [0, 1.661333333333332], + [-3.098666666666645, 0.541333333333327], + [-1.680000000000064, 0.2987000000000108], + [-0.7839999999999918, 0.7467000000000041], + [0, 1.157300000000006], + [0.8959999999999582, 0.7092999999999989], + [1.717999999999961, 0], + [1.307000000000016, -0.7467000000000041], + [0.7100000000000364, -1.2693000000000012], + [0, -2.594666666666669], + [0, -1.493299999999991], + [-1.081999999999994, -0.9707000000000079], + [-1.680000000000064, 0], + [-1.232000000000085, 0.8586999999999989], + [-1.754666666666708, -0.8773333333333255], + [0.5599999999999454, -1.493300000000005], + [1.269999999999982, -1.157300000000006], + [1.717999999999961, -0.6347000000000094], + [2.389999999999986, 0], + [1.866999999999962, 0.8960000000000008], + [1.045999999999935, 1.568000000000012], + [0, 6.794666666666672], + [-2.015999999999963, 0], + [0, -1.829333333333338], + [0.4293333333333749, 0.05599999999999739], + [-0.70900000000006, 1.2319999999999993], + [-1.081999999999994, 0.8960000000000008], + [-1.380999999999972, 0.4853000000000094], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [159.072, 42.392], + [159.072, 0], + [165.4, 0], + [165.4, 42.392], + [159.072, 42.392], + [159.072, 42.392], + [159.072, 42.392] + ], + "i": [ + [0, 0], + [0, 14.13066666666667], + [-2.109333333333325, 0], + [0, -14.13066666666667], + [2.109333333333325, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, -14.13066666666667], + [2.109333333333325, 0], + [0, 14.13066666666667], + [-2.109333333333325, 0], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [138.952, 43.06400000000001], + [130.888, 40.992], + [125.456, 35.28], + [123.496, 27.16], + [125.456, 19.040000000000006], + [130.832, 13.49600000000001], + [138.448, 11.424000000000007], + [144.552, 12.600000000000009], + [149.088, 15.848], + [151.888, 20.49600000000001], + [152.896, 26.096], + [152.84, 27.608], + [152.616, 29.064000000000007], + [128.48, 29.064000000000007], + [128.48, 24.024], + [149.032, 24.024], + [146.008, 26.320000000000007], + [145.616, 21.448000000000008], + [142.816, 18.032], + [138.448, 16.744], + [133.968, 18.032], + [130.944, 21.616], + [130.104, 27.216000000000008], + [130.944, 32.592], + [134.192, 36.176], + [139.008, 37.464], + [143.65599999999995, 36.232], + [146.736, 33.040000000000006], + [151.888, 35.56], + [149.088, 39.42400000000001], + [144.60799999999995, 42.11200000000001], + [138.952, 43.06400000000001], + [138.952, 43.06400000000001], + [138.952, 43.06400000000001] + ], + "i": [ + [0, 0], + [2.351999999999975, 1.381299999999996], + [1.307000000000016, 2.389300000000006], + [0, 2.986699999999999], + [-1.307000000000016, 2.35199999999999], + [-2.240000000000009, 1.343999999999994], + [-2.836999999999989, 0], + [-1.79200000000003, -0.784000000000006], + [-1.231999999999971, -1.381299999999996], + [-0.6349999999999909, -1.754700000000014], + [0, -1.978700000000003], + [0.03699999999992087, -0.5227000000000004], + [0.1119999999999663, -0.4480000000000075], + [8.04533333333336, 0], + [0, 1.680000000000007], + [-6.850666666666712, 0], + [1.008000000000038, -0.7653333333333308], + [0.6349999999999909, 1.4187000000000012], + [1.269000000000005, 0.8213000000000079], + [1.680000000000064, 0], + [1.307000000000016, -0.8586999999999989], + [0.70900000000006, -1.567999999999998], + [-0.1490000000000009, -2.202700000000007], + [-0.7469999999999573, -1.530699999999996], + [-1.380999999999972, -0.8586999999999989], + [-1.79200000000003, 0], + [-1.268999999999892, 0.8213000000000079], + [-0.7469999999999573, 1.306699999999992], + [-1.717333333333386, -0.8400000000000034], + [1.269000000000005, -1.157300000000006], + [1.755000000000109, -0.6720000000000113], + [2.052999999999997, 0], + [0, 0], + [0, 0] + ], + "o": [ + [-3.024000000000001, 0], + [-2.315000000000055, -1.4187000000000012], + [-1.307000000000016, -2.426699999999997], + [0, -3.061299999999989], + [1.343999999999937, -2.352000000000004], + [2.240000000000009, -1.3813000000000102], + [2.277000000000044, 0], + [1.79200000000003, 0.7839999999999918], + [1.232000000000085, 1.344000000000008], + [0.6720000000000255, 1.7547], + [0, 0.4852999999999952], + [-0.03700000000003456, 0.5227000000000004], + [-8.04533333333336, 0], + [0, -1.680000000000007], + [6.850666666666712, 0], + [-1.008000000000038, 0.7653333333333308], + [0.3729999999999336, -1.829300000000003], + [-0.59699999999998, -1.456000000000003], + [-1.232000000000085, -0.8586999999999989], + [-1.67999999999995, 0], + [-1.306999999999903, 0.8213000000000079], + [-0.7089999999999463, 1.530699999999996], + [-0.1870000000000118, 2.053299999999993], + [0.7839999999999918, 1.53070000000001], + [1.418999999999983, 0.8586999999999989], + [1.828999999999951, 0], + [1.307000000000016, -0.8212999999999937], + [1.717333333333386, 0.8400000000000034], + [-0.59699999999998, 1.4187000000000012], + [-1.231999999999971, 1.11999999999999], + [-1.716999999999985, 0.6346999999999952], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [111.001, 7.951999999999998], + [111.001, 0.6720000000000041], + [117.329, 0.6720000000000041], + [117.329, 7.951999999999998], + [111.001, 7.951999999999998], + [111.001, 7.951999999999998], + [111.001, 7.951999999999998] + ], + "i": [ + [0, 0], + [0, 2.426666666666662], + [-2.109333333333325, 0], + [0, -2.426666666666662], + [2.109333333333325, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, -2.426666666666662], + [2.109333333333325, 0], + [0, 2.426666666666662], + [-2.109333333333325, 0], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [111.001, 42.392], + [111.001, 12.096], + [117.329, 12.096], + [117.329, 42.392], + [111.001, 42.392], + [111.001, 42.392], + [111.001, 42.392] + ], + "i": [ + [0, 0], + [0, 10.09866666666667], + [-2.109333333333325, 0], + [0, -10.09866666666667], + [2.109333333333325, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, -10.09866666666666], + [2.109333333333325, 0], + [0, 10.09866666666666], + [-2.109333333333325, 0], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [101.41, 42.72800000000001], + [94.01800000000003, 40.040000000000006], + [91.38599999999997, 32.48], + [91.38599999999997, 17.808000000000007], + [86.06600000000003, 17.808000000000007], + [86.06600000000003, 12.096], + [86.90599999999995, 12.096], + [90.21000000000004, 10.864], + [91.38599999999997, 7.504000000000005], + [91.38599999999997, 5.152000000000001], + [97.71400000000006, 5.152000000000001], + [97.71400000000006, 12.096], + [104.602, 12.096], + [104.602, 17.808000000000007], + [97.71400000000006, 17.808000000000007], + [97.71400000000006, 32.2], + [98.21799999999996, 34.888000000000005], + [99.84199999999998, 36.568], + [102.754, 37.128], + [103.76200000000006, 37.072], + [104.826, 36.96000000000001], + [104.826, 42.392], + [103.09, 42.616], + [101.41, 42.72800000000001], + [101.41, 42.72800000000001], + [101.41, 42.72800000000001] + ], + "i": [ + [0, 0], + [1.754999999999995, 1.792000000000002], + [0, 3.248000000000005], + [0, 4.890666666666661], + [1.773333333333312, 0], + [0, 1.903999999999996], + [-0.2799999999999727, 0], + [-0.7839999999999918, 0.8212999999999937], + [0, 1.4187000000000012], + [0, 0.7839999999999989], + [-2.109333333333325, 0], + [0, -2.314666666666668], + [-2.295999999999935, 0], + [0, -1.903999999999996], + [2.295999999999935, 0], + [0, -4.797333333333327], + [-0.3360000000000127, -0.7467000000000041], + [-0.7469999999999573, -0.4106999999999914], + [-1.19500000000005, 0], + [-0.3730000000000473, 0.03730000000000189], + [-0.3360000000000127, 0.03729999999998768], + [0, -1.810666666666663], + [0.6349999999999909, -0.07469999999999288], + [0.4850000000000136, 0], + [0, 0], + [0, 0] + ], + "o": [ + [-3.173000000000002, 0], + [-1.754999999999995, -1.792000000000002], + [0, -4.890666666666661], + [-1.773333333333312, 0], + [0, -1.903999999999996], + [0.2799999999999727, 0], + [1.419000000000096, 0], + [0.7839999999999918, -0.8213000000000079], + [0, -0.7839999999999989], + [2.109333333333325, 0], + [0, 2.314666666666668], + [2.295999999999935, 0], + [0, 1.903999999999996], + [-2.295999999999935, 0], + [0, 4.797333333333327], + [0, 1.045299999999997], + [0.3360000000000127, 0.7092999999999989], + [0.7470000000000709, 0.3733000000000004], + [0.2989999999999782, 0], + [0.3729999999999336, -0.03730000000000189], + [0, 1.810666666666663], + [-0.5230000000000246, 0.0747000000000071], + [-0.6349999999999909, 0.0747000000000071], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [78.71499999999997, 42.72800000000001], + [71.32299999999998, 40.040000000000006], + [68.69100000000003, 32.48], + [68.69100000000003, 17.808000000000007], + [63.37099999999998, 17.808000000000007], + [63.37099999999998, 12.096], + [64.21100000000001, 12.096], + [67.51499999999999, 10.864], + [68.69100000000003, 7.504000000000005], + [68.69100000000003, 5.152000000000001], + [75.019, 5.152000000000001], + [75.019, 12.096], + [81.90700000000004, 12.096], + [81.90700000000004, 17.808000000000007], + [75.019, 17.808000000000007], + [75.019, 32.2], + [75.52300000000002, 34.888000000000005], + [77.14699999999999, 36.568], + [80.05900000000003, 37.128], + [81.06700000000001, 37.072], + [82.13099999999997, 36.96000000000001], + [82.13099999999997, 42.392], + [80.39499999999998, 42.616], + [78.71499999999997, 42.72800000000001], + [78.71499999999997, 42.72800000000001], + [78.71499999999997, 42.72800000000001] + ], + "i": [ + [0, 0], + [1.754000000000019, 1.792000000000002], + [0, 3.248000000000005], + [0, 4.890666666666661], + [1.773333333333369, 0], + [0, 1.903999999999996], + [-0.2800000000000296, 0], + [-0.7839999999999918, 0.8212999999999937], + [0, 1.4187000000000012], + [0, 0.7839999999999989], + [-2.109333333333325, 0], + [0, -2.314666666666668], + [-2.295999999999992, 0], + [0, -1.903999999999996], + [2.295999999999992, 0], + [0, -4.797333333333327], + [-0.3360000000000127, -0.7467000000000041], + [-0.7470000000000141, -0.4106999999999914], + [-1.19500000000005, 0], + [-0.3740000000000236, 0.03730000000000189], + [-0.3360000000000127, 0.03729999999998768], + [0, -1.810666666666663], + [0.6340000000000146, -0.07469999999999288], + [0.4850000000000136, 0], + [0, 0], + [0, 0] + ], + "o": [ + [-3.173999999999978, 0], + [-1.754999999999995, -1.792000000000002], + [0, -4.890666666666661], + [-1.773333333333369, 0], + [0, -1.903999999999996], + [0.2800000000000296, 0], + [1.418000000000006, 0], + [0.7839999999999918, -0.8213000000000079], + [0, -0.7839999999999989], + [2.109333333333325, 0], + [0, 2.314666666666668], + [2.295999999999992, 0], + [0, 1.903999999999996], + [-2.295999999999992, 0], + [0, 4.797333333333327], + [0, 1.045299999999997], + [0.3359999999999559, 0.7092999999999989], + [0.7460000000000377, 0.3733000000000004], + [0.297999999999945, 0], + [0.3730000000000473, -0.03730000000000189], + [0, 1.810666666666663], + [-0.5230000000000246, 0.0747000000000071], + [-0.6349999999999909, 0.0747000000000071], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [44.18799999999999, 37.184], + [48.94799999999998, 35.896], + [52.19600000000003, 32.36800000000001], + [53.428, 27.216000000000008], + [52.19600000000003, 22.12], + [48.94799999999998, 18.592], + [44.18799999999999, 17.304], + [39.428, 18.592], + [36.12400000000002, 22.12], + [34.94799999999998, 27.216000000000008], + [36.12400000000002, 32.36800000000001], + [39.428, 35.896], + [44.18799999999999, 37.184], + [44.18799999999999, 37.184], + [44.18799999999999, 37.184] + ], + "i": [ + [0, 0], + [-1.381999999999948, 0.8586999999999989], + [-0.7840000000000487, 1.493299999999991], + [0, 1.903999999999996], + [0.8209999999999695, 1.493299999999991], + [1.381000000000029, 0.8586999999999989], + [1.79200000000003, 0], + [1.418000000000006, -0.8586999999999989], + [0.8209999999999695, -1.493300000000005], + [0, -1.904000000000011], + [-0.7840000000000487, -1.5307000000000102], + [-1.382000000000005, -0.8586999999999989], + [-1.754999999999995, 0], + [0, 0], + [0, 0] + ], + "o": [ + [1.79200000000003, 0], + [1.381000000000029, -0.8586999999999989], + [0.8209999999999695, -1.5307000000000102], + [0, -1.904000000000011], + [-0.7840000000000487, -1.493300000000005], + [-1.381999999999948, -0.8586999999999989], + [-1.754999999999995, 0], + [-1.382000000000005, 0.8586999999999989], + [-0.7840000000000487, 1.493299999999991], + [0, 1.903999999999996], + [0.8209999999999695, 1.493299999999991], + [1.418000000000006, 0.8586999999999989], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [44.18799999999999, 43.06400000000001], + [36.18000000000001, 40.992], + [30.46800000000002, 35.336], + [28.33999999999997, 27.216000000000008], + [30.46800000000002, 19.096], + [36.18000000000001, 13.49600000000001], + [44.18799999999999, 11.424000000000007], + [52.19600000000003, 13.49600000000001], + [57.85199999999998, 19.096], + [59.98000000000002, 27.216000000000008], + [57.85199999999998, 35.392], + [52.139999999999986, 41.048], + [44.18799999999999, 43.06400000000001], + [44.18799999999999, 43.06400000000001], + [44.18799999999999, 43.06400000000001] + ], + "i": [ + [0, 0], + [2.425999999999988, 1.381299999999996], + [1.418000000000006, 2.389300000000006], + [0, 3.024000000000001], + [-1.41900000000004, 2.352000000000004], + [-2.389999999999986, 1.343999999999994], + [-2.949999999999989, 0], + [-2.352000000000032, -1.3813000000000102], + [-1.381999999999948, -2.389300000000006], + [0, -3.061300000000003], + [1.418000000000006, -2.389299999999992], + [2.38900000000001, -1.381299999999996], + [2.912000000000035, 0], + [0, 0], + [0, 0] + ], + "o": [ + [-2.911999999999978, 0], + [-2.389999999999986, -1.381299999999996], + [-1.41900000000004, -2.389299999999992], + [0, -3.061300000000003], + [1.418000000000006, -2.389300000000006], + [2.38900000000001, -1.3813000000000102], + [2.98599999999999, 0], + [2.388999999999953, 1.343999999999994], + [1.418000000000006, 2.352000000000004], + [0, 3.061299999999989], + [-1.418999999999983, 2.389300000000006], + [-2.389999999999986, 1.343999999999994], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [0, 42.392], + [0, 0.6720000000000041], + [6.608000000000004, 0.6720000000000041], + [6.608000000000004, 36.512], + [24.639999999999986, 36.512], + [24.639999999999986, 42.392], + [0, 42.392], + [0, 42.392], + [0, 42.392] + ], + "i": [ + [0, 0], + [0, 13.90666666666666], + [-2.202666666666687, 0], + [0, -11.94666666666666], + [-6.01066666666668, 0], + [0, -1.959999999999994], + [8.21333333333331, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, -13.90666666666667], + [2.202666666666687, 0], + [0, 11.94666666666667], + [6.01066666666668, 0], + [0, 1.959999999999994], + [-8.21333333333331, 0], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "fl", + "c": { "a": 0, "k": [1, 1, 1], "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "r": 1, + "bm": 0 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [98.08047485351562, -21.67217254638672], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [99.99999403953552, 99.99999403953552], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [246.681, 42.392], + [246.681, 0], + [253.009, 0], + [253.009, 18.032], + [252.001, 17.248], + [255.585, 12.936000000000007], + [261.297, 11.424000000000007], + [267.23299999999995, 12.88], + [271.265, 16.912000000000006], + [272.721, 22.792], + [272.721, 42.392], + [266.449, 42.392], + [266.449, 24.528000000000006], + [265.553, 20.664], + [263.201, 18.2], + [259.729, 17.304], + [256.25699999999995, 18.2], + [253.849, 20.664], + [253.009, 24.528000000000006], + [253.009, 42.392], + [246.681, 42.392], + [246.681, 42.392], + [246.681, 42.392] + ], + "i": [ + [0, 0], + [0, 14.13066666666667], + [-2.109333333333325, 0], + [0, -6.010666666666665], + [0.3360000000000127, 0.2613333333333259], + [-1.643000000000029, 0.9706999999999937], + [-2.166000000000054, 0], + [-1.717999999999961, -0.9706999999999937], + [-0.9710000000000036, -1.717300000000009], + [0, -2.202699999999993], + [0, -6.533333333333331], + [2.090666666666721, 0], + [0, 5.954666666666668], + [0.59699999999998, 1.045299999999997], + [1.007999999999925, 0.5600000000000023], + [1.305999999999926, 0], + [1.045000000000073, -0.5973000000000042], + [0.59699999999998, -1.082700000000003], + [0, -1.493300000000005], + [0, -5.954666666666668], + [2.109333333333325, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, -14.13066666666667], + [2.109333333333325, 0], + [0, 6.010666666666665], + [-0.3360000000000127, -0.2613333333333259], + [0.7459999999999809, -1.903999999999996], + [1.641999999999967, -1.0080000000000098], + [2.240000000000009, 0], + [1.717000000000098, 0.9707000000000079], + [0.9700000000000273, 1.717299999999994], + [0, 6.533333333333331], + [-2.090666666666721, 0], + [0, -5.954666666666668], + [0, -1.5307000000000102], + [-0.5599999999999454, -1.082700000000003], + [-1.008000000000038, -0.5973000000000042], + [-1.270000000000095, 0], + [-1.007999999999953, 0.5600000000000023], + [-0.5600000000000023, 1.082700000000003], + [0, 5.954666666666668], + [-2.109333333333325, 0], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-354.5325317382812, -77.50520324707031], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [237.089, 42.72800000000001], + [229.697, 40.040000000000006], + [227.065, 32.48], + [227.065, 17.808000000000007], + [221.745, 17.808000000000007], + [221.745, 12.096], + [222.585, 12.096], + [225.889, 10.864], + [227.065, 7.504000000000005], + [227.065, 5.152000000000001], + [233.393, 5.152000000000001], + [233.393, 12.096], + [240.281, 12.096], + [240.281, 17.808000000000007], + [233.393, 17.808000000000007], + [233.393, 32.2], + [233.897, 34.888000000000005], + [235.521, 36.568], + [238.433, 37.128], + [239.441, 37.072], + [240.505, 36.96000000000001], + [240.505, 42.392], + [238.769, 42.616], + [237.089, 42.72800000000001], + [237.089, 42.72800000000001], + [237.089, 42.72800000000001] + ], + "i": [ + [0, 0], + [1.755000000000052, 1.792000000000002], + [0, 3.248000000000005], + [0, 4.890666666666661], + [1.773333333333369, 0], + [0, 1.903999999999996], + [-0.2800000000000296, 0], + [-0.7839999999999918, 0.8212999999999937], + [0, 1.4187000000000012], + [0, 0.7839999999999989], + [-2.109333333333325, 0], + [0, -2.314666666666668], + [-2.295999999999992, 0], + [0, -1.903999999999996], + [2.295999999999992, 0], + [0, -4.797333333333327], + [-0.3360000000000127, -0.7467000000000041], + [-0.7459999999999809, -0.4106999999999914], + [-1.194000000000017, 0], + [-0.3729999999999905, 0.03730000000000189], + [-0.3360000000000127, 0.03729999999998768], + [0, -1.810666666666663], + [0.6350000000000477, -0.07469999999999288], + [0.48599999999999, 0], + [0, 0], + [0, 0] + ], + "o": [ + [-3.173000000000002, 0], + [-1.753999999999962, -1.792000000000002], + [0, -4.890666666666661], + [-1.773333333333369, 0], + [0, -1.903999999999996], + [0.2800000000000296, 0], + [1.418999999999983, 0], + [0.7839999999999918, -0.8213000000000079], + [0, -0.7839999999999989], + [2.109333333333325, 0], + [0, 2.314666666666668], + [2.295999999999992, 0], + [0, 1.903999999999996], + [-2.295999999999992, 0], + [0, 4.797333333333327], + [0, 1.045299999999997], + [0.3359999999999559, 0.7092999999999989], + [0.7470000000000141, 0.3733000000000004], + [0.2989999999999782, 0], + [0.3740000000000236, -0.03730000000000189], + [0, 1.810666666666663], + [-0.5220000000000482, 0.0747000000000071], + [-0.6339999999999577, 0.0747000000000071], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-354.5325317382812, -77.50520324707031], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [210.259, 7.951999999999998], + [210.259, 0.6720000000000041], + [216.587, 0.6720000000000041], + [216.587, 7.951999999999998], + [210.259, 7.951999999999998], + [210.259, 7.951999999999998], + [210.259, 7.951999999999998] + ], + "i": [ + [0, 0], + [0, 2.426666666666662], + [-2.109333333333325, 0], + [0, -2.426666666666662], + [2.109333333333325, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, -2.426666666666662], + [2.109333333333325, 0], + [0, 2.426666666666662], + [-2.109333333333325, 0], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-354.5325317382812, -77.50520324707031], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [210.259, 42.392], + [210.259, 12.096], + [216.587, 12.096], + [216.587, 42.392], + [210.259, 42.392], + [210.259, 42.392], + [210.259, 42.392] + ], + "i": [ + [0, 0], + [0, 10.09866666666667], + [-2.109333333333325, 0], + [0, -10.09866666666667], + [2.109333333333325, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, -10.09866666666666], + [2.109333333333325, 0], + [0, 10.09866666666666], + [-2.109333333333325, 0], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-354.5325317382812, -77.50520324707031], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [169.688, 42.392], + [159.272, 12.096], + [165.992, 12.096], + [173.944, 36.232], + [171.592, 36.232], + [179.712, 12.096], + [185.48, 12.096], + [193.544, 36.232], + [191.192, 36.232], + [199.2, 12.096], + [205.92, 12.096], + [195.448, 42.392], + [189.736, 42.392], + [181.56, 17.696], + [183.632, 17.696], + [175.456, 42.392], + [169.688, 42.392], + [169.688, 42.392], + [169.688, 42.392] + ], + "i": [ + [0, 0], + [3.47199999999998, 10.09866666666667], + [-2.240000000000009, 0], + [-2.650666666666666, -8.045333333333332], + [0.7839999999999918, 0], + [-2.706666666666649, 8.045333333333332], + [-1.922666666666657, 0], + [-2.687999999999988, -8.045333333333332], + [0.7839999999999918, 0], + [-2.669333333333327, 8.045333333333332], + [-2.240000000000009, 0], + [3.490666666666641, -10.09866666666667], + [1.903999999999996, 0], + [2.725333333333367, 8.232], + [-0.6906666666666865, 0], + [2.72533333333331, -8.232], + [1.922666666666657, 0], + [0, 0], + [0, 0] + ], + "o": [ + [-3.47199999999998, -10.09866666666666], + [2.240000000000009, 0], + [2.650666666666666, 8.045333333333332], + [-0.7839999999999918, 0], + [2.706666666666649, -8.045333333333332], + [1.922666666666657, 0], + [2.687999999999988, 8.045333333333332], + [-0.7839999999999918, 0], + [2.669333333333327, -8.045333333333332], + [2.240000000000009, 0], + [-3.490666666666641, 10.09866666666666], + [-1.903999999999996, 0], + [-2.725333333333367, -8.232], + [0.6906666666666865, 0], + [-2.72533333333331, 8.232], + [-1.922666666666657, 0], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-354.5325317382812, -77.50520324707031], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "fl", + "c": { "a": 0, "k": [1, 1, 1], "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "r": 1, + "bm": 0 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [155.86146545410156, 56.001014709472656], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [99.99999403953552, 99.99999403953552], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [132.444, 43.06400000000001], + [124.38, 40.992], + [118.948, 35.28], + [116.988, 27.16], + [118.948, 19.040000000000006], + [124.324, 13.49600000000001], + [131.94, 11.424000000000007], + [138.044, 12.600000000000009], + [142.58, 15.848], + [145.38, 20.49600000000001], + [146.388, 26.096], + [146.332, 27.608], + [146.108, 29.064000000000007], + [121.972, 29.064000000000007], + [121.972, 24.024], + [142.524, 24.024], + [139.5, 26.320000000000007], + [139.108, 21.448000000000008], + [136.308, 18.032], + [131.94, 16.744], + [127.46, 18.032], + [124.436, 21.616], + [123.596, 27.216000000000008], + [124.436, 32.592], + [127.684, 36.176], + [132.5, 37.464], + [137.148, 36.232], + [140.228, 33.040000000000006], + [145.38, 35.56], + [142.58, 39.42400000000001], + [138.1, 42.11200000000001], + [132.444, 43.06400000000001], + [132.444, 43.06400000000001], + [132.444, 43.06400000000001] + ], + "i": [ + [0, 0], + [2.351999999999975, 1.381299999999996], + [1.305999999999983, 2.389300000000006], + [0, 2.986699999999999], + [-1.307000000000016, 2.35199999999999], + [-2.240000000000009, 1.343999999999994], + [-2.838000000000022, 0], + [-1.79200000000003, -0.784000000000006], + [-1.232000000000028, -1.381299999999996], + [-0.6350000000000477, -1.754700000000014], + [0, -1.978700000000003], + [0.03699999999997772, -0.5227000000000004], + [0.1120000000000232, -0.4480000000000075], + [8.045333333333303, 0], + [0, 1.680000000000007], + [-6.850666666666655, 0], + [1.007999999999981, -0.7653333333333308], + [0.6340000000000146, 1.4187000000000012], + [1.269000000000005, 0.8213000000000079], + [1.67999999999995, 0], + [1.305999999999983, -0.8586999999999989], + [0.7090000000000032, -1.567999999999998], + [-0.1499999999999773, -2.202700000000007], + [-0.7470000000000141, -1.530699999999996], + [-1.382000000000005, -0.8586999999999989], + [-1.791999999999973, 0], + [-1.269999999999982, 0.8213000000000079], + [-0.7469999999999573, 1.306699999999992], + [-1.717333333333329, -0.8400000000000034], + [1.269000000000005, -1.157300000000006], + [1.754000000000019, -0.6720000000000113], + [2.052999999999997, 0], + [0, 0], + [0, 0] + ], + "o": [ + [-3.024000000000001, 0], + [-2.314999999999998, -1.4187000000000012], + [-1.307000000000016, -2.426699999999997], + [0, -3.061299999999989], + [1.343999999999994, -2.352000000000004], + [2.240000000000009, -1.3813000000000102], + [2.276999999999987, 0], + [1.791999999999973, 0.7839999999999918], + [1.231999999999971, 1.344000000000008], + [0.6719999999999686, 1.7547], + [0, 0.4852999999999952], + [-0.03800000000001091, 0.5227000000000004], + [-8.045333333333303, 0], + [0, -1.680000000000007], + [6.850666666666655, 0], + [-1.007999999999981, 0.7653333333333308], + [0.3730000000000473, -1.829300000000003], + [-0.5979999999999563, -1.456000000000003], + [-1.232000000000028, -0.8586999999999989], + [-1.680000000000007, 0], + [-1.307000000000016, 0.8213000000000079], + [-0.7100000000000364, 1.530699999999996], + [-0.186999999999955, 2.053299999999993], + [0.7839999999999918, 1.53070000000001], + [1.418000000000006, 0.8586999999999989], + [1.829000000000008, 0], + [1.305999999999983, -0.8212999999999937], + [1.717333333333329, 0.8400000000000034], + [-0.5980000000000132, 1.4187000000000012], + [-1.232000000000028, 1.11999999999999], + [-1.718000000000018, 0.6346999999999952], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-211.7300415039062, -77.67320251464844], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [95.32, 37.184], + [100.024, 35.896], + [103.328, 32.36800000000001], + [104.56, 27.216000000000008], + [103.328, 22.12], + [100.024, 18.592], + [95.32, 17.304], + [90.56, 18.592], + [87.256, 22.12], + [86.08000000000001, 27.216000000000008], + [87.256, 32.36800000000001], + [90.50399999999999, 35.896], + [95.32, 37.184], + [95.32, 37.184], + [95.32, 37.184] + ], + "i": [ + [0, 0], + [-1.381, 0.8586999999999989], + [-0.7839999999999918, 1.493299999999991], + [0, 1.903999999999996], + [0.820999999999998, 1.493299999999991], + [1.419000000000011, 0.8586999999999989], + [1.754999999999995, 0], + [1.418999999999983, -0.8586999999999989], + [0.7839999999999918, -1.493300000000005], + [0, -1.904000000000011], + [-0.7839999999999918, -1.5307000000000102], + [-1.381, -0.8586999999999989], + [-1.792000000000002, 0], + [0, 0], + [0, 0] + ], + "o": [ + [1.754999999999995, 0], + [1.419000000000011, -0.8586999999999989], + [0.820999999999998, -1.5307000000000102], + [0, -1.904000000000011], + [-0.7839999999999918, -1.493300000000005], + [-1.381, -0.8586999999999989], + [-1.754999999999995, 0], + [-1.419000000000011, 0.8586999999999989], + [-0.7839999999999918, 1.493299999999991], + [0, 1.903999999999996], + [0.7839999999999918, 1.493299999999991], + [1.419000000000011, 0.8586999999999989], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-211.7300415039062, -77.67320251464844], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [94.70400000000001, 43.06400000000001], + [86.864, 40.992], + [81.43199999999999, 35.28], + [79.47200000000001, 27.216000000000008], + [81.488, 19.152], + [86.91999999999999, 13.49600000000001], + [94.648, 11.424000000000007], + [101.088, 12.88], + [105.512, 16.912000000000006], + [104.56, 18.36800000000001], + [104.56, 0], + [110.832, 0], + [110.832, 42.392], + [104.84, 42.392], + [104.84, 36.232], + [105.568, 37.408], + [101.088, 41.608], + [94.70400000000001, 43.06400000000001], + [94.70400000000001, 43.06400000000001], + [94.70400000000001, 43.06400000000001] + ], + "i": [ + [0, 0], + [2.314999999999998, 1.381299999999996], + [1.344000000000023, 2.389300000000006], + [0, 2.986699999999999], + [-1.343999999999994, 2.389300000000006], + [-2.276999999999987, 1.381299999999996], + [-2.875, 0], + [-1.8669999999999902, -0.9706999999999937], + [-1.082999999999998, -1.717300000000009], + [0.3173333333333233, -0.4853333333333296], + [0, 6.122666666666674], + [-2.090666666666664, 0], + [0, -14.13066666666667], + [1.99733333333333, 0], + [0, 2.053333333333327], + [-0.242666666666679, -0.3919999999999959], + [1.941000000000003, -0.9707000000000079], + [2.314999999999998, 0], + [0, 0], + [0, 0] + ], + "o": [ + [-2.912000000000006, 0], + [-2.277000000000015, -1.4187000000000012], + [-1.306999999999988, -2.389300000000006], + [0, -2.986699999999999], + [1.343999999999994, -2.389300000000006], + [2.277000000000015, -1.3813000000000102], + [2.426999999999992, 0], + [1.867000000000019, 0.9707000000000079], + [-0.3173333333333233, 0.4853333333333296], + [0, -6.122666666666674], + [2.090666666666664, 0], + [0, 14.13066666666667], + [-1.99733333333333, 0], + [0, -2.053333333333327], + [0.242666666666679, 0.3919999999999959], + [-1.045000000000016, 1.829300000000003], + [-1.941000000000003, 0.9706999999999937], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-211.7300415039062, -77.67320251464844], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [57.40100000000001, 43.06400000000001], + [51.968999999999994, 42], + [48.38499999999999, 38.976], + [47.09700000000001, 34.384], + [48.161, 29.960000000000008], + [51.577, 26.656000000000006], + [57.345, 24.752], + [67.87299999999999, 23.016000000000005], + [67.87299999999999, 28], + [58.577, 29.624], + [54.881, 31.248], + [53.70500000000001, 34.16], + [54.992999999999995, 37.016000000000005], + [58.40899999999999, 38.08], + [62.88900000000001, 36.96000000000001], + [65.91300000000001, 33.992], + [66.977, 29.792], + [66.977, 22.008], + [65.297, 18.36800000000001], + [60.929, 16.912000000000006], + [56.505, 18.256], + [53.761, 21.616], + [48.496999999999986, 18.98400000000001], + [51.240999999999985, 15.064000000000007], + [55.66499999999999, 12.376], + [61.09700000000001, 11.424000000000007], + [67.42500000000001, 12.768], + [71.737, 16.52], + [73.305, 22.008], + [73.305, 42.392], + [67.257, 42.392], + [67.257, 36.904], + [68.54499999999999, 37.072], + [65.80099999999999, 40.264], + [62.04900000000001, 42.336], + [57.40100000000001, 43.06400000000001], + [57.40100000000001, 43.06400000000001], + [57.40100000000001, 43.06400000000001] + ], + "i": [ + [0, 0], + [1.568000000000012, 0.7092999999999989], + [0.8590000000000089, 1.2693000000000012], + [0, 1.7547], + [-0.7090000000000032, 1.306699999999992], + [-1.531000000000006, 0.8960000000000008], + [-2.314999999999998, 0.3733000000000004], + [-3.509333333333331, 0.5786666666666633], + [0, -1.661333333333332], + [3.098666666666674, -0.541333333333327], + [0.7839999999999918, -0.784000000000006], + [0, -1.194699999999997], + [-0.8590000000000089, -0.7467000000000041], + [-1.381, 0], + [-1.269000000000005, 0.7466999999999899], + [-0.7090000000000032, 1.2319999999999993], + [0, 1.530699999999996], + [0, 2.594666666666669], + [1.120000000000005, 0.9332999999999885], + [1.829000000000008, 0], + [1.269000000000005, -0.8960000000000008], + [0.5970000000000084, -1.381299999999996], + [1.754666666666679, 0.8773333333333255], + [-1.268999999999977, 1.11999999999999], + [-1.680000000000007, 0.6346999999999952], + [-1.903999999999996, 0], + [-1.829000000000008, -0.8960000000000008], + [-1.0080000000000098, -1.6053], + [0, -2.090699999999998], + [0, -6.794666666666672], + [2.015999999999991, 0], + [0, 1.829333333333338], + [-0.429333333333318, -0.05599999999999739], + [1.120000000000005, -0.8959999999999866], + [1.418999999999983, -0.4852999999999952], + [1.716999999999985, 0], + [0, 0], + [0, 0] + ], + "o": [ + [-2.053000000000026, 0], + [-1.531000000000006, -0.7467000000000041], + [-0.8589999999999804, -1.306699999999992], + [0, -1.642700000000005], + [0.7469999999999857, -1.306700000000006], + [1.531000000000006, -0.8960000000000008], + [3.509333333333331, -0.5786666666666633], + [0, 1.661333333333332], + [-3.098666666666674, 0.541333333333327], + [-1.680000000000007, 0.2987000000000108], + [-0.7839999999999918, 0.7467000000000041], + [0, 1.157300000000006], + [0.896000000000015, 0.7092999999999989], + [1.717000000000013, 0], + [1.306999999999988, -0.7467000000000041], + [0.7089999999999748, -1.2693000000000012], + [0, -2.594666666666669], + [0, -1.493299999999991], + [-1.082999999999998, -0.9707000000000079], + [-1.680000000000007, 0], + [-1.2319999999999993, 0.8586999999999989], + [-1.754666666666679, -0.8773333333333255], + [0.5600000000000023, -1.493300000000005], + [1.269000000000005, -1.157300000000006], + [1.717000000000013, -0.6347000000000094], + [2.388999999999982, 0], + [1.86699999999999, 0.8960000000000008], + [1.045000000000016, 1.568000000000012], + [0, 6.794666666666672], + [-2.015999999999991, 0], + [0, -1.829333333333338], + [0.429333333333318, 0.05599999999999739], + [-0.7089999999999748, 1.2319999999999993], + [-1.082999999999998, 0.8960000000000008], + [-1.381, 0.4853000000000094], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-211.7300415039062, -77.67320251464844], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [0, 42.392], + [0, 0.6720000000000041], + [6.159999999999997, 0.6720000000000041], + [21.84, 22.400000000000006], + [18.75999999999999, 22.400000000000006], + [34.16, 0.6720000000000041], + [40.31999999999999, 0.6720000000000041], + [40.31999999999999, 42.392], + [33.768, 42.392], + [33.768, 8.456000000000003], + [36.232, 9.128], + [20.49600000000001, 30.632000000000005], + [19.824000000000012, 30.632000000000005], + [4.424000000000007, 9.128], + [6.608000000000004, 8.456000000000003], + [6.608000000000004, 42.392], + [0, 42.392], + [0, 42.392], + [0, 42.392] + ], + "i": [ + [0, 0], + [0, 13.90666666666666], + [-2.053333333333342, 0], + [-5.226666666666659, -7.242666666666665], + [1.026666666666671, 0], + [-5.133333333333326, 7.242666666666672], + [-2.053333333333342, 0], + [0, -13.90666666666667], + [2.183999999999997, 0], + [0, 11.312], + [-0.8213333333333424, -0.2240000000000038], + [5.245333333333321, -7.168000000000006], + [0.2239999999999895, 0], + [5.133333333333326, 7.168000000000006], + [-0.7280000000000086, 0.2240000000000038], + [0, -11.312], + [2.202666666666659, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, -13.90666666666667], + [2.053333333333342, 0], + [5.226666666666659, 7.242666666666672], + [-1.026666666666671, 0], + [5.133333333333326, -7.242666666666665], + [2.053333333333342, 0], + [0, 13.90666666666666], + [-2.183999999999997, 0], + [0, -11.312], + [0.8213333333333424, 0.2240000000000038], + [-5.245333333333321, 7.168000000000006], + [-0.2239999999999895, 0], + [-5.133333333333326, -7.168000000000006], + [0.7280000000000086, -0.2240000000000038], + [0, 11.312], + [-2.202666666666659, 0], + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-211.7300415039062, -77.67320251464844], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "fl", + "c": { "a": 0, "k": [1, 1, 1], "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "r": 1, + "bm": 0 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [2.91259765625, 56.001014709472656], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [99.99999403953552, 99.99999403953552], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "gr", + "it": [ + { + "ty": "rc", + "d": 1, + "s": { "a": 0, "k": [702.6863719370097, 144], "ix": 2 }, + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "r": { "a": 0, "k": 72, "ix": 2 } + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0, 0, 0], "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "r": 1, + "bm": 0 + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [56.54167175292969, -0.000022762338630855083], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [99.99999403953552, 99.99999403953552], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 80, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "tr", + "p": { "a": 0, "k": [122.0000003294881, 25.00000012138912], "ix": 2 }, + "a": { "a": 0, "k": [56.54167175292969, -0.00002288818359375], "ix": 2 }, + "s": { "a": 0, "k": [34.403572049765366, 34.403572049765366], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + } + ], + "ip": 0, + "op": 251, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "line_06", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "nm": "line_06", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": false, + "v": [ + [12, 12], + [49.732, 12] + ], + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { + "a": 1, + "k": [ + { + "t": 163, + "s": [0], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + }, + { + "t": 183, + "s": [100], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "w": { "a": 0, "k": 24, "ix": 2 }, + "lc": 2, + "lj": 1, + "ml": 4 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [159.64700317382812, 386.0762634277344], "ix": 2 }, + "a": { "a": 0, "k": [30.866, 12], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + } + ], + "ip": 0, + "op": 251, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "line_05", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "nm": "line_05", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": false, + "v": [ + [12, 12], + [133.386, 12] + ], + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { + "a": 1, + "k": [ + { + "t": 140, + "s": [0], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + }, + { + "t": 160, + "s": [100], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "w": { "a": 0, "k": 24, "ix": 2 }, + "lc": 2, + "lj": 1, + "ml": 4 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [201.4739990234375, 305.88525390625], "ix": 2 }, + "a": { "a": 0, "k": [72.693, 12], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + } + ], + "ip": 0, + "op": 251, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "line_04", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "nm": "line_04", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": false, + "v": [ + [12, 12], + [217.895, 12] + ], + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { + "a": 1, + "k": [ + { + "t": 120, + "s": [0], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + }, + { + "t": 143, + "s": [100], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "w": { "a": 0, "k": 24, "ix": 2 }, + "lc": 2, + "lj": 1, + "ml": 4 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [243.728515625, 225.6952362060547], "ix": 2 }, + "a": { "a": 0, "k": [114.9475, 12], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + } + ], + "ip": 0, + "op": 251, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 4, + "ty": 4, + "nm": "line_03", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "nm": "line_03", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "v": [ + [-36.047, -71.491], + [-57.706, -49.832], + [-57.303, 12.081], + [-0.871, 71.477], + [0.204, 71.491], + [57.706, 13.985], + [57.304, -49.832], + [35.649, -71.491], + [-36.047, -71.491] + ], + "i": [ + [0, 0], + [0, -11.962], + [0, 0], + [-31.661, -0.575], + [-0.356, 0], + [0, 31.76], + [0, 0], + [11.962, 0], + [0, 0] + ], + "o": [ + [-11.962, 0], + [0, 0], + [0, 31.661], + [0.357, 0.01], + [31.761, 0], + [0, 0], + [0, -11.962], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { + "a": 1, + "k": [ + { + "t": 0, + "s": [0], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + }, + { + "t": 43, + "s": [100], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "e": { "a": 0, "k": 0, "ix": 2 }, + "o": { + "a": 1, + "k": [ + { + "t": 0, + "s": [0], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + }, + { + "t": 43, + "s": [121.00000000000001], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "m": 1 + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "w": { "a": 0, "k": 24, "ix": 2 }, + "lc": 2, + "lj": 1, + "ml": 4 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [243, 93.62023162841797], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + } + ], + "ip": 0, + "op": 251, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 5, + "ty": 4, + "nm": "line_02", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "nm": "line_02", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": false, + "v": [ + [186.878, 129.076], + [100.884, 215.071], + [-100.885, 215.071], + [-186.878, 129.076], + [-186.878, -129.076], + [-100.885, -215.07], + [-61.924, -215.07] + ], + "i": [ + [0, 0], + [47.496, 0], + [0, 0], + [0, 47.491], + [0, 0], + [-47.491, 0], + [0, 0] + ], + "o": [ + [0, 47.491], + [0, 0], + [-47.491, 0], + [0, 0], + [0, -47.491], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 100, "ix": 2 }, + "e": { + "a": 1, + "k": [ + { + "t": 33, + "s": [100], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + }, + { + "t": 70, + "s": [0], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "o": { + "a": 1, + "k": [ + { + "t": 33, + "s": [16], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + }, + { + "t": 70, + "s": [126], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "m": 1 + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "w": { "a": 0, "k": 24, "ix": 2 }, + "lc": 2, + "lj": 1, + "ml": 4 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [245.79200744628906, 289.354736328125], "ix": 2 }, + "a": { "a": 0, "k": [0, 0.0004999999999881766], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + } + ], + "ip": 0, + "op": 251, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 6, + "ty": 4, + "nm": "line_01", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "nm": "line_01", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": false, + "v": [ + [-63.344, -131.277], + [-22.649, -131.277], + [63.344, -45.283], + [63.344, 131.277] + ], + "i": [ + [0, 0], + [0, 0], + [0, -47.491], + [0, 0] + ], + "o": [ + [0, 0], + [47.492, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { + "a": 1, + "k": [ + { + "t": 50, + "s": [0], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + }, + { + "t": 127, + "s": [100], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "o": { + "a": 1, + "k": [ + { + "t": 50, + "s": [0], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + }, + { + "t": 87, + "s": [316], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "m": 1 + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "w": { "a": 0, "k": 24, "ix": 2 }, + "lc": 2, + "lj": 1, + "ml": 4 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [369.3269958496094, 205.56024169921875], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + } + ], + "ip": 0, + "op": 251, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 7, + "ty": 4, + "nm": "correct", + "sr": 1, + "ks": { + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "nm": "Group 1", + "it": [ + { + "ty": "sh", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": false, + "v": [ + [-56.365, -4.925], + [-9.754, 41.501], + [56.365, -41.501] + ], + "i": [ + [0, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0], + [0, 0] + ] + } + } + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 2 }, + "e": { + "a": 1, + "k": [ + { + "t": 140, + "s": [0], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + }, + { + "t": 167, + "s": [100], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "o": { "a": 0, "k": 0, "ix": 2 }, + "m": 1 + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "w": { "a": 0, "k": 24, "ix": 2 }, + "lc": 2, + "lj": 2, + "ml": 4 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [116.365, 101.5], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + }, + { + "ty": "tr", + "p": { + "a": 1, + "k": [ + { + "t": 140, + "s": [288.5679931640625, 424.9672546386719], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] }, + "ti": [0, 2.167], + "to": [0, 2.667] + }, + { + "t": 167, + "s": [288.5679931640625, 440.9672546386719], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] }, + "ti": [0, 1.333], + "to": [0, -2.167] + }, + { + "t": 180, + "s": [288.5679931640625, 411.9672546386719], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] }, + "ti": [0, -1.667], + "to": [0, -1.333] + }, + { + "t": 200, + "s": [288.5679931640625, 432.9672546386719], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] }, + "ti": [0, 0.833], + "to": [0, 1.667] + }, + { + "t": 213, + "s": [288.5679931640625, 421.96725463867193], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] }, + "ti": [0, -0.5], + "to": [0, -0.833] + }, + { + "t": 227, + "s": [288.5679931640625, 427.9672546386719], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] }, + "ti": [0, 0.5], + "to": [0, 0.5] + }, + { + "t": 237, + "s": [288.5679931640625, 424.9672546386719], + "i": { "x": [0.75], "y": [0.75] }, + "o": { "x": [0.25], "y": [0.25] } + } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [105.365, 142.5], "ix": 2 }, + "s": { "a": 0, "k": [100, 100], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 2 }, + "o": { "a": 0, "k": 100, "ix": 2 }, + "sk": { "a": 0, "k": 0, "ix": 2 }, + "sa": { "a": 0, "k": 0, "ix": 2 } + } + ] + } + ], + "ip": 0, + "op": 251, + "st": 0, + "bm": 0 + } + ], + "markers": [] +} diff --git a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx index ea331f866..afd63d114 100644 --- a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx +++ b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx @@ -126,7 +126,11 @@ export const ProjectLayout = () => { }} > {({ isActive }) => ( - + Certificate Templates )} From 09db9e340b821005e54022ca93397727a3436041 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 30 May 2025 22:38:56 +0530 Subject: [PATCH 7/9] feat: review comments addressed --- .../server/routes/v2/pki-templates-router.ts | 4 +-- .../pki-templates/pki-templates-service.ts | 30 ++++++++++++------- .../pki-templates/pki-templates-types.ts | 4 +-- .../hooks/api/certificateTemplates/types.ts | 4 +-- .../PkiTemplateListPage.tsx | 2 +- .../components/PkiTemplateForm.tsx | 5 ++-- 6 files changed, 30 insertions(+), 19 deletions(-) diff --git a/backend/src/server/routes/v2/pki-templates-router.ts b/backend/src/server/routes/v2/pki-templates-router.ts index b6b969706..e481af0a2 100644 --- a/backend/src/server/routes/v2/pki-templates-router.ts +++ b/backend/src/server/routes/v2/pki-templates-router.ts @@ -26,7 +26,7 @@ export const registerPkiTemplatesRouter = async (server: FastifyZodProvider) => tags: [ApiDocsTags.PkiCertificateTemplates], body: z.object({ name: slugSchema(), - caId: z.string(), + caName: slugSchema({ field: "caName" }), projectId: z.string(), commonName: validateTemplateRegexField, subjectAlternativeName: validateTemplateRegexField, @@ -72,7 +72,7 @@ export const registerPkiTemplatesRouter = async (server: FastifyZodProvider) => }), body: z.object({ name: slugSchema().optional(), - caId: z.string(), + caName: slugSchema(), projectId: z.string(), commonName: validateTemplateRegexField.optional(), subjectAlternativeName: validateTemplateRegexField.optional(), diff --git a/backend/src/services/pki-templates/pki-templates-service.ts b/backend/src/services/pki-templates/pki-templates-service.ts index e72a2198a..97f910d6e 100644 --- a/backend/src/services/pki-templates/pki-templates-service.ts +++ b/backend/src/services/pki-templates/pki-templates-service.ts @@ -56,7 +56,13 @@ type TPkiTemplatesServiceFactoryDep = { permissionService: Pick; certificateAuthorityDAL: Pick< TCertificateAuthorityDALFactory, - "findByIdWithAssociatedCa" | "findById" | "transaction" | "create" | "updateById" | "findWithAssociatedCa" + | "findByIdWithAssociatedCa" + | "findById" + | "transaction" + | "create" + | "updateById" + | "findWithAssociatedCa" + | "findOne" >; internalCaFns: ReturnType; kmsService: Pick; @@ -92,20 +98,22 @@ export const pkiTemplatesServiceFactory = ({ actorId, actorAuthMethod, actorOrgId, - caId, + caName, commonName, extendedKeyUsages, keyUsages, name, subjectAlternativeName, - ttl + ttl, + projectId }: TCreatePkiTemplateDTO) => { - const ca = await certificateAuthorityDAL.findById(caId); + const ca = await certificateAuthorityDAL.findOne({ name: caName, projectId }); if (!ca) { throw new NotFoundError({ - message: `CA with ID ${caId} not found` + message: `CA with name ${caName} not found` }); } + const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -126,7 +134,7 @@ export const pkiTemplatesServiceFactory = ({ } const newTemplate = await pkiTemplatesDAL.create({ - caId, + caId: ca.id, name, commonName, subjectAlternativeName, @@ -143,7 +151,7 @@ export const pkiTemplatesServiceFactory = ({ actorId, actorAuthMethod, actorOrgId, - caId, + caName, commonName, extendedKeyUsages, keyUsages, @@ -173,13 +181,15 @@ export const pkiTemplatesServiceFactory = ({ subject(ProjectPermissionSub.CertificateTemplates, { name: templateName }) ); - if (caId) { - const ca = await certificateAuthorityDAL.findById(caId); + let caId; + if (caName) { + const ca = await certificateAuthorityDAL.findOne({ name: caName, projectId }); if (!ca || ca.projectId !== certTemplate.projectId) { throw new NotFoundError({ - message: `CA with ID ${caId} not found` + message: `CA with name ${caName} not found` }); } + caId = ca.id; } if (name) { diff --git a/backend/src/services/pki-templates/pki-templates-types.ts b/backend/src/services/pki-templates/pki-templates-types.ts index 72d245de1..8dd18c8a9 100644 --- a/backend/src/services/pki-templates/pki-templates-types.ts +++ b/backend/src/services/pki-templates/pki-templates-types.ts @@ -2,7 +2,7 @@ import { TProjectPermission } from "@app/lib/types"; import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types"; export type TCreatePkiTemplateDTO = { - caId: string; + caName: string; name: string; commonName: string; subjectAlternativeName: string; @@ -13,7 +13,7 @@ export type TCreatePkiTemplateDTO = { export type TUpdatePkiTemplateDTO = { templateName: string; - caId?: string; + caName?: string; name?: string; commonName?: string; subjectAlternativeName?: string; diff --git a/frontend/src/hooks/api/certificateTemplates/types.ts b/frontend/src/hooks/api/certificateTemplates/types.ts index 3e3e728f8..1c2a47178 100644 --- a/frontend/src/hooks/api/certificateTemplates/types.ts +++ b/frontend/src/hooks/api/certificateTemplates/types.ts @@ -65,7 +65,7 @@ export type TDeleteCertificateTemplateDTO = { }; export type TCreateCertificateTemplateV2DTO = { - caId: string; + caName: string; name: string; commonName: string; subjectAlternativeName: string; @@ -77,7 +77,7 @@ export type TCreateCertificateTemplateV2DTO = { export type TUpdateCertificateTemplateV2DTO = { templateName: string; - caId?: string; + caName?: string; name?: string; commonName?: string; subjectAlternativeName?: string; diff --git a/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx b/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx index 7adf86bc7..895775360 100644 --- a/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx +++ b/frontend/src/pages/cert-manager/PkiTemplateListPage/PkiTemplateListPage.tsx @@ -130,7 +130,7 @@ export const PkiTemplateListPage = () => { Name - CA + Issuing CA Last Updated At diff --git a/frontend/src/pages/cert-manager/PkiTemplateListPage/components/PkiTemplateForm.tsx b/frontend/src/pages/cert-manager/PkiTemplateListPage/components/PkiTemplateForm.tsx index 0a78ddac2..19022b4b9 100644 --- a/frontend/src/pages/cert-manager/PkiTemplateListPage/components/PkiTemplateForm.tsx +++ b/frontend/src/pages/cert-manager/PkiTemplateListPage/components/PkiTemplateForm.tsx @@ -1,4 +1,5 @@ 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"; @@ -133,7 +134,7 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => { await updateCertTemplate({ templateName: certTemplate.name, projectId: currentWorkspace.id, - caId: ca.id, + caName: ca.name, name, commonName, subjectAlternativeName, @@ -153,7 +154,7 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => { } else { await createCertTemplate({ projectId: currentWorkspace.id, - caId: ca.id, + caName: ca.name, name, commonName, subjectAlternativeName, From 4efbb8dca64c9ba1a484d15f24f6004e00853d16 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Fri, 30 May 2025 17:54:57 +0000 Subject: [PATCH 8/9] fix: resolved merge conflict --- backend/src/ee/services/permission/default-roles.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index f0993b30c..0eb8f3cf8 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -68,12 +68,10 @@ const buildAdminPermissionRules = () => { can( [ - ProjectPermissionApprovalActions.Read, - ProjectPermissionApprovalActions.Edit, - ProjectPermissionApprovalActions.Create, - ProjectPermissionApprovalActions.Delete, - ProjectPermissionApprovalActions.AllowChangeBypass, - ProjectPermissionApprovalActions.AllowAccessBypass + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete ], ProjectPermissionSub.SecretApproval ); From 5c0e265703d340ba449da1bf8fc6b25bf9372938 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Fri, 30 May 2025 18:03:04 +0000 Subject: [PATCH 9/9] fix: resolved merge conflict --- backend/src/ee/services/oidc/oidc-config-service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index 7cebc1825..d933835e4 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -15,7 +15,6 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/pe import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors"; -import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";