From 71a76bc9412fd1e8f9eaf49783fd74ffc095d41f Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Wed, 3 Dec 2025 00:05:18 -0300 Subject: [PATCH 1/9] PKI refine access control --- .../certificate-authority-crl-service.ts | 7 +- .../ee/services/permission/default-roles.ts | 30 +- .../services/permission/project-permission.ts | 223 +++++++- .../certificate-authority-service.ts | 53 +- .../internal-certificate-authority-service.ts | 42 +- .../certificate-profile-service.ts | 72 ++- .../certificate-template-v2-service.ts | 35 +- .../certificate-v3/certificate-v3-service.ts | 20 +- .../certificate/certificate-service.ts | 60 +- .../src/services/pki-sync/pki-sync-service.ts | 52 +- .../src/services/project/project-service.ts | 5 +- .../ProjectPermissionContext/index.tsx | 1 + .../context/ProjectPermissionContext/types.ts | 69 ++- frontend/src/context/index.tsx | 1 + .../src/hooks/api/auditLogs/constants.tsx | 6 +- .../CertAuthDetailsByIDPage.tsx | 135 +++-- .../CaCertificatesSection.tsx | 5 +- .../CaCertificatesTable.tsx | 18 +- .../components/CaDetailsSection.tsx | 16 +- .../components/CaSection.tsx | 8 +- .../components/CaTable.tsx | 50 +- .../components/ExternalCaSection.tsx | 8 +- .../components/ExternalCaTable.tsx | 51 +- .../components/CertificateIssuanceModal.tsx | 6 +- .../components/CertificateModal.tsx | 2 +- .../components/CertificatesTable.tsx | 57 +- .../PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx | 512 +++++++++--------- .../components/PkiSyncActionTriggers.tsx | 4 +- .../components/PkiSyncCertificatesSection.tsx | 7 +- .../components/PkiSyncDestinationSection.tsx | 4 +- .../components/PkiSyncDetailsSection.tsx | 7 +- .../PkiSyncFieldMappingsSection.tsx | 6 +- .../PkiSyncOptionsSection.tsx | 7 +- .../components/PkiSyncSourceSection.tsx | 7 +- .../CertificateProfilesTab.tsx | 37 +- .../CertificateProfilesTab/ProfileRow.tsx | 138 ++--- .../CertificateTemplatesV2Tab.tsx | 35 +- .../TemplateList.tsx | 114 ++-- ...rtificateAuthorityPermissionConditions.tsx | 19 + .../CertificatePermissionConditions.tsx | 24 + ...CertificateProfilePermissionConditions.tsx | 19 + .../PkiSyncPermissionConditions.tsx | 2 +- .../ProjectRoleModifySection.utils.tsx | 146 +++-- .../components/RolePermissionsSection.tsx | 15 + 44 files changed, 1459 insertions(+), 676 deletions(-) create mode 100644 frontend/src/pages/project/RoleDetailsBySlugPage/components/CertificateAuthorityPermissionConditions.tsx create mode 100644 frontend/src/pages/project/RoleDetailsBySlugPage/components/CertificatePermissionConditions.tsx create mode 100644 frontend/src/pages/project/RoleDetailsBySlugPage/components/CertificateProfilePermissionConditions.tsx diff --git a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts index 5ead798fa..81657e15d 100644 --- a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts +++ b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts @@ -4,7 +4,10 @@ import * as x509 from "@peculiar/x509"; 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-types"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionCertificateAuthorityActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { NotFoundError } from "@app/lib/errors"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { expandInternalCa } from "@app/services/certificate-authority/certificate-authority-fns"; @@ -83,7 +86,7 @@ export const certificateAuthorityCrlServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionCertificateAuthorityActions.Read, ProjectPermissionSub.CertificateAuthorities ); diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index 81814a67c..a0d63caff 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -5,6 +5,7 @@ import { ProjectPermissionAppConnectionActions, ProjectPermissionAuditLogsActions, ProjectPermissionCertificateActions, + ProjectPermissionCertificateAuthorityActions, ProjectPermissionCertificateProfileActions, ProjectPermissionCmekActions, ProjectPermissionCommitsActions, @@ -46,7 +47,6 @@ const buildAdminPermissionRules = () => { ProjectPermissionSub.Tags, ProjectPermissionSub.AuditLogs, ProjectPermissionSub.IpAllowList, - ProjectPermissionSub.CertificateAuthorities, ProjectPermissionSub.PkiAlerts, ProjectPermissionSub.PkiCollections, ProjectPermissionSub.SshCertificateAuthorities, @@ -67,6 +67,19 @@ const buildAdminPermissionRules = () => { ); }); + can( + [ + ProjectPermissionCertificateAuthorityActions.Read, + ProjectPermissionCertificateAuthorityActions.Create, + ProjectPermissionCertificateAuthorityActions.Edit, + ProjectPermissionCertificateAuthorityActions.Delete, + ProjectPermissionCertificateAuthorityActions.List, + ProjectPermissionCertificateAuthorityActions.Renew, + ProjectPermissionCertificateAuthorityActions.SignIntermediate + ], + ProjectPermissionSub.CertificateAuthorities + ); + can( [ ProjectPermissionPkiTemplateActions.Read, @@ -95,7 +108,9 @@ const buildAdminPermissionRules = () => { ProjectPermissionCertificateActions.Edit, ProjectPermissionCertificateActions.Create, ProjectPermissionCertificateActions.Delete, - ProjectPermissionCertificateActions.ReadPrivateKey + ProjectPermissionCertificateActions.ReadPrivateKey, + ProjectPermissionCertificateActions.Import, + ProjectPermissionCertificateActions.List ], ProjectPermissionSub.Certificates ); @@ -103,6 +118,7 @@ const buildAdminPermissionRules = () => { can( [ ProjectPermissionCertificateProfileActions.Read, + ProjectPermissionCertificateProfileActions.List, ProjectPermissionCertificateProfileActions.Edit, ProjectPermissionCertificateProfileActions.Create, ProjectPermissionCertificateProfileActions.Delete, @@ -460,7 +476,7 @@ const buildMemberPermissionRules = () => { can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList); // double check if all CRUD are needed for CA and Certificates - can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities); + can([ProjectPermissionCertificateAuthorityActions.Read], ProjectPermissionSub.CertificateAuthorities); can([ProjectPermissionPkiTemplateActions.Read], ProjectPermissionSub.CertificateTemplates); can( @@ -468,7 +484,9 @@ const buildMemberPermissionRules = () => { ProjectPermissionCertificateActions.Read, ProjectPermissionCertificateActions.Edit, ProjectPermissionCertificateActions.Create, - ProjectPermissionCertificateActions.Delete + ProjectPermissionCertificateActions.Delete, + ProjectPermissionCertificateActions.Import, + ProjectPermissionCertificateActions.List ], ProjectPermissionSub.Certificates ); @@ -476,6 +494,7 @@ const buildMemberPermissionRules = () => { can( [ ProjectPermissionCertificateProfileActions.Read, + ProjectPermissionCertificateProfileActions.List, ProjectPermissionCertificateProfileActions.Edit, ProjectPermissionCertificateProfileActions.Create, ProjectPermissionCertificateProfileActions.Delete @@ -599,8 +618,9 @@ const buildViewerPermissionRules = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); can(ProjectPermissionAuditLogsActions.Read, ProjectPermissionSub.AuditLogs); can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); - can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities); + can(ProjectPermissionCertificateAuthorityActions.Read, ProjectPermissionSub.CertificateAuthorities); can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates); + can(ProjectPermissionCertificateActions.List, ProjectPermissionSub.Certificates); can(ProjectPermissionPkiTemplateActions.Read, ProjectPermissionSub.CertificateTemplates); can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 19340644a..5a744e064 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -23,12 +23,24 @@ export enum ProjectPermissionCommitsActions { PerformRollback = "perform-rollback" } +export enum ProjectPermissionCertificateAuthorityActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + List = "list", + Renew = "renew", + SignIntermediate = "sign-intermediate" +} + export enum ProjectPermissionCertificateActions { Read = "read", Create = "create", Edit = "edit", Delete = "delete", - ReadPrivateKey = "read-private-key" + List = "list", + ReadPrivateKey = "read-private-key", + Import = "import" } export enum ProjectPermissionSecretActions { @@ -117,6 +129,7 @@ export enum ProjectPermissionPkiSubscriberActions { export enum ProjectPermissionCertificateProfileActions { Read = "read", + List = "list", Create = "create", Edit = "edit", Delete = "delete", @@ -140,6 +153,7 @@ export enum ProjectPermissionPkiSyncActions { Create = "create", Edit = "edit", Delete = "delete", + List = "list", SyncCertificates = "sync-certificates", ImportCertificates = "import-certificates", RemoveCertificates = "remove-certificates" @@ -242,6 +256,7 @@ export enum ProjectPermissionSub { CertificateAuthorities = "certificate-authorities", Certificates = "certificates", CertificateTemplates = "certificate-templates", + CertificateTemplatesV2 = "certificate-templates-v2", SshCertificateAuthorities = "ssh-certificate-authorities", SshCertificates = "ssh-certificates", SshCertificateTemplates = "ssh-certificate-templates", @@ -292,7 +307,7 @@ export type SecretSyncSubjectFields = { }; export type PkiSyncSubjectFields = { - subscriberName: string; + name: string; }; export type DynamicSecretSubjectFields = { @@ -332,6 +347,26 @@ export type PkiSubscriberSubjectFields = { // (dangtony98): consider adding [commonName] as a subject field in the future }; +export type CertificateAuthoritySubjectFields = { + name: string; +}; + +export type CertificateSubjectFields = { + commonName?: string; + altNames?: string; + serialNumber?: string; + friendlyName?: string; + status?: string; +}; + +export type CertificateProfileSubjectFields = { + slug: string; +}; + +export type CertificateTemplateV2SubjectFields = { + name: string; +}; + export type AppConnectionSubjectFields = { connectionId: string; }; @@ -399,8 +434,17 @@ export type ProjectPermissionSet = ProjectPermissionIdentityActions, ProjectPermissionSub.Identity | (ForcedSubject & IdentityManagementSubjectFields) ] - | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] - | [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates] + | [ + ProjectPermissionCertificateAuthorityActions, + ( + | ProjectPermissionSub.CertificateAuthorities + | (ForcedSubject & CertificateAuthoritySubjectFields) + ) + ] + | [ + ProjectPermissionCertificateActions, + ProjectPermissionSub.Certificates | (ForcedSubject & CertificateSubjectFields) + ] | [ ProjectPermissionPkiTemplateActions, ( @@ -408,6 +452,13 @@ export type ProjectPermissionSet = | (ForcedSubject & PkiTemplateSubjectFields) ) ] + | [ + ProjectPermissionActions, + ( + | ProjectPermissionSub.CertificateTemplatesV2 + | (ForcedSubject & CertificateTemplateV2SubjectFields) + ) + ] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] @@ -454,7 +505,13 @@ export type ProjectPermissionSet = ProjectPermissionSub.PamAccounts | (ForcedSubject & PamAccountSubjectFields) ] | [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions] - | [ProjectPermissionCertificateProfileActions, ProjectPermissionSub.CertificateProfiles]; + | [ + ProjectPermissionCertificateProfileActions, + ( + | ProjectPermissionSub.CertificateProfiles + | (ForcedSubject & CertificateProfileSubjectFields) + ) + ]; const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'"; const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([ @@ -572,7 +629,7 @@ const SecretSyncConditionV2Schema = z const PkiSyncConditionSchema = z .object({ - subscriberName: z.union([ + name: z.union([ z.string(), z .object({ @@ -749,6 +806,113 @@ const PamAccountConditionSchema = z }) .partial(); +const CertificateAuthorityConditionSchema = z + .object({ + name: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] + }) + .partial() + ]) + }) + .partial(); + +const CertificateConditionSchema = z + .object({ + commonName: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] + }) + .partial() + ]), + altNames: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] + }) + .partial() + ]), + serialNumber: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] + }) + .partial() + ]), + friendlyName: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] + }) + .partial() + ]), + status: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] + }) + .partial() + ]) + }) + .partial(); + +const CertificateProfileConditionSchema = z + .object({ + slug: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] + }) + .partial() + ]) + }) + .partial(); + +const CertificateTemplateV2ConditionSchema = z + .object({ + name: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] + }) + .partial() + ]) + }) + .partial(); + const GeneralPermissionSchema = [ z.object({ subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), @@ -828,18 +992,6 @@ const GeneralPermissionSchema = [ "Describe what action an entity can take." ) }), - z.object({ - subject: z.literal(ProjectPermissionSub.CertificateAuthorities).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.Certificates).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateActions).describe( - "Describe what action an entity can take." - ) - }), z.object({ subject: z .literal(ProjectPermissionSub.SshCertificateAuthorities) @@ -1130,7 +1282,40 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ inverted: z.boolean().optional().describe("Whether rule allows or forbids."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateProfileActions).describe( "Describe what action an entity can take." - ) + ), + conditions: CertificateProfileConditionSchema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), + z.object({ + subject: z.literal(ProjectPermissionSub.CertificateAuthorities).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateAuthorityActions).describe( + "Describe what action an entity can take." + ), + conditions: CertificateAuthorityConditionSchema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), + z.object({ + subject: z.literal(ProjectPermissionSub.Certificates).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateActions).describe( + "Describe what action an entity can take." + ), + conditions: CertificateConditionSchema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), + z.object({ + subject: z.literal(ProjectPermissionSub.CertificateTemplatesV2).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + "Describe what action an entity can take." + ), + conditions: CertificateTemplateV2ConditionSchema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() }), ...GeneralPermissionSchema ]); diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index b9b3b8d42..e217a9ffc 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -1,8 +1,11 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType, TableName } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionCertificateAuthorityActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; @@ -137,8 +140,8 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Create, + subject(ProjectPermissionSub.CertificateAuthorities, { name }) ); if (type === CaType.INTERNAL) { @@ -207,8 +210,8 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Read, + subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name }) ); if (type === CaType.INTERNAL) { @@ -222,6 +225,7 @@ export const certificateAuthorityServiceFactory = ({ id: certificateAuthority.id, type, enableDirectIssuance: certificateAuthority.enableDirectIssuance, + subject: ProjectPermissionSub.CertificateAuthorities, name: certificateAuthority.name, projectId: certificateAuthority.projectId, configuration: certificateAuthority.internalCa, @@ -270,8 +274,8 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Read, + subject(ProjectPermissionSub.CertificateAuthorities, { name: caName }) ); if (type === CaType.INTERNAL) { @@ -323,8 +327,8 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Read, + subject(ProjectPermissionSub.CertificateAuthorities, { name: "*" }) ); if (type === CaType.INTERNAL) { @@ -378,8 +382,8 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Edit, + subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name }) ); if (type === CaType.INTERNAL) { @@ -454,8 +458,8 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Delete, + subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name }) ); if (!certificateAuthority.internalCa?.id && type === CaType.INTERNAL) { @@ -519,8 +523,8 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Edit, + subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name }) ); if (type === CaType.INTERNAL) { @@ -601,8 +605,8 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Delete, + subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name }) ); if (!certificateAuthority.internalCa?.id && type === CaType.INTERNAL) { @@ -657,6 +661,13 @@ export const certificateAuthorityServiceFactory = ({ actorAuthMethod: OrgServiceActor["authMethod"]; actorOrgId?: string; }) => { + const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); + + if (!certificateAuthority) + throw new NotFoundError({ + message: `Could not find certificate authority with id "${caId}"` + }); + const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -667,8 +678,10 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Read, + subject(ProjectPermissionSub.CertificateAuthorities, { + name: certificateAuthority.name + }) ); return azureAdCsFns.getTemplates({ 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 35c596cd3..e24dbbbf7 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 @@ -7,8 +7,8 @@ import { Knex } from "knex"; import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { - ProjectPermissionActions, ProjectPermissionCertificateActions, + ProjectPermissionCertificateAuthorityActions, ProjectPermissionCertificateProfileActions, ProjectPermissionPkiTemplateActions, ProjectPermissionSub @@ -173,8 +173,8 @@ export const internalCertificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Create, + subject(ProjectPermissionSub.CertificateAuthorities, { name: commonName }) ); } else { projectId = dto.projectId; @@ -356,8 +356,8 @@ export const internalCertificateAuthorityServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Read, + subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name }) ); return expandInternalCa(ca); @@ -382,8 +382,8 @@ export const internalCertificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Edit, + subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name }) ); } @@ -415,8 +415,8 @@ export const internalCertificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Delete, + subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name }) ); await certificateAuthorityDAL.deleteById(ca.id); @@ -441,8 +441,8 @@ export const internalCertificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Create, + subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name }) ); if (ca.internalCa.type === InternalCaType.ROOT) @@ -505,8 +505,8 @@ export const internalCertificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Renew, + subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name }) ); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); @@ -792,8 +792,8 @@ export const internalCertificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Read, + subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name }) ); const caCertChains = await getCaCertChains({ @@ -829,8 +829,8 @@ export const internalCertificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Read, + subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name }) ); const { caCert, caCertChain, serialNumber } = await getCaCertChain({ @@ -910,8 +910,8 @@ export const internalCertificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.SignIntermediate, + subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name }) ); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); @@ -1058,8 +1058,8 @@ export const internalCertificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Create, + subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name }) ); if (ca.internalCa.parentCaId) { diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index 0ce3ca9b6..bd1f2a4d2 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -1,4 +1,4 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import { ActionProjectType } from "@app/db/schemas"; @@ -235,7 +235,9 @@ export const certificateProfileServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.Create, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug: data.slug + }) ); const project = await projectDAL.findById(projectId); @@ -381,7 +383,9 @@ export const certificateProfileServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.Edit, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug: existingProfile.slug + }) ); if (data.certificateTemplateId) { @@ -494,7 +498,9 @@ export const certificateProfileServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.Read, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug: profile.slug + }) ); const converted = convertDalToService(profile); @@ -530,7 +536,9 @@ export const certificateProfileServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.Read, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug: profile.slug + }) ); if (profile.estConfig && profile.estConfig.caChain) { @@ -589,7 +597,9 @@ export const certificateProfileServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.Read, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug + }) ); const profile = await certificateProfileDAL.findBySlugAndProjectId(slug, projectId); @@ -637,7 +647,7 @@ export const certificateProfileServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateProfileActions.Read, + ProjectPermissionCertificateProfileActions.List, ProjectPermissionSub.CertificateProfiles ); @@ -740,7 +750,9 @@ export const certificateProfileServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.Delete, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug: profile.slug + }) ); const deletedProfile = await certificateProfileDAL.deleteById(profileId); @@ -786,7 +798,9 @@ export const certificateProfileServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.Read, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug: profile.slug + }) ); const certificates = await certificateProfileDAL.getCertificatesByProfile(profileId, { @@ -827,17 +841,9 @@ export const certificateProfileServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.Read, - ProjectPermissionSub.CertificateProfiles - ); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateActions.Read, - ProjectPermissionSub.Certificates - ); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateActions.ReadPrivateKey, - ProjectPermissionSub.Certificates + subject(ProjectPermissionSub.CertificateProfiles, { + slug: profile.slug + }) ); const cert = await certificateProfileDAL.getLatestActiveCertificateForProfile(profileId); @@ -846,6 +852,24 @@ export const certificateProfileServiceFactory = ({ return null; } + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.Read, + subject(ProjectPermissionSub.Certificates, { + commonName: cert.commonName, + altNames: cert.altNames ?? undefined, + serialNumber: cert.serialNumber + }) + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.ReadPrivateKey, + subject(ProjectPermissionSub.Certificates, { + commonName: cert.commonName, + altNames: cert.altNames ?? undefined, + serialNumber: cert.serialNumber + }) + ); + const certBody = await certificateBodyDAL.findOne({ certId: cert.id }); const certificateManagerKeyId = await getProjectKmsCertificateKeyId({ @@ -939,7 +963,9 @@ export const certificateProfileServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.Read, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug: profile.slug + }) ); } @@ -990,7 +1016,9 @@ export const certificateProfileServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug: profile.slug + }) ); if (profile.enrollmentType !== EnrollmentType.ACME) { diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts index 656c12e64..9cc3d22c4 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts @@ -1,4 +1,4 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; import RE2 from "re2"; @@ -77,12 +77,12 @@ export const certificateTemplateV2ServiceFactory = ({ }; const validateSubjectAttributePolicy = ( - subject: Array<{ type: string; allowed?: string[]; required?: string[]; denied?: string[] }> + subjectAttributes: Array<{ type: string; allowed?: string[]; required?: string[]; denied?: string[] }> ) => { - if (!subject || subject.length === 0) return; + if (!subjectAttributes || subjectAttributes.length === 0) return; // Validate each subject attribute policy - for (const attr of subject) { + for (const attr of subjectAttributes) { // Ensure at least one field is provided if (!attr.allowed && !attr.required && !attr.denied) { throw new ForbiddenRequestError({ @@ -634,7 +634,9 @@ export const certificateTemplateV2ServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiTemplateActions.Create, - ProjectPermissionSub.CertificateTemplates + subject(ProjectPermissionSub.CertificateTemplates, { + name: data.name + }) ); if (!data) { @@ -711,7 +713,9 @@ export const certificateTemplateV2ServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiTemplateActions.Edit, - ProjectPermissionSub.CertificateTemplates + subject(ProjectPermissionSub.CertificateTemplates, { + name: existingTemplate.name + }) ); const consolidatedData = { @@ -784,7 +788,9 @@ export const certificateTemplateV2ServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiTemplateActions.Read, - ProjectPermissionSub.CertificateTemplates + subject(ProjectPermissionSub.CertificateTemplates, { + name: template.name + }) ); } @@ -815,16 +821,17 @@ export const certificateTemplateV2ServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionPkiTemplateActions.Read, - ProjectPermissionSub.CertificateTemplates - ); - const template = await certificateTemplateV2DAL.findByNameAndProjectId(slug, projectId); if (!template) { throw new NotFoundError({ message: "Certificate template not found" }); } + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiTemplateActions.Read, + subject(ProjectPermissionSub.CertificateTemplates, { + name: template.name + }) + ); return template; }; @@ -907,7 +914,9 @@ export const certificateTemplateV2ServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiTemplateActions.Delete, - ProjectPermissionSub.CertificateTemplates + subject(ProjectPermissionSub.CertificateTemplates, { + name: template.name + }) ); const isInUse = await certificateTemplateV2DAL.isTemplateInUse(templateId); diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index b648b5478..14a424961 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -1,4 +1,4 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import { randomUUID } from "crypto"; import RE2 from "re2"; @@ -155,7 +155,9 @@ const validateProfileAndPermissions = async ( ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.IssueCert, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug: profile.slug + }) ); return profile; @@ -1275,7 +1277,7 @@ export const certificateV3ServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.IssueCert, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { slug: profile?.slug || "*" }) ); } @@ -1568,7 +1570,11 @@ export const certificateV3ServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateActions.Edit, - ProjectPermissionSub.Certificates + subject(ProjectPermissionSub.Certificates, { + commonName: certificate.commonName, + altNames: certificate.altNames ?? undefined, + serialNumber: certificate.serialNumber + }) ); if (!certificate.profileId) { @@ -1671,7 +1677,11 @@ export const certificateV3ServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateActions.Edit, - ProjectPermissionSub.Certificates + subject(ProjectPermissionSub.Certificates, { + commonName: certificate.commonName, + altNames: certificate.altNames ?? undefined, + serialNumber: certificate.serialNumber + }) ); if (!certificate.profileId) { diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index 44be47fc7..7ea6d03cd 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -1,5 +1,5 @@ /* eslint-disable no-await-in-loop */ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import { ActionProjectType } from "@app/db/schemas"; @@ -108,7 +108,11 @@ export const certificateServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateActions.Read, - ProjectPermissionSub.Certificates + subject(ProjectPermissionSub.Certificates, { + commonName: cert.commonName, + altNames: cert.altNames ?? undefined, + serialNumber: cert.serialNumber + }) ); return { @@ -140,7 +144,11 @@ export const certificateServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateActions.ReadPrivateKey, - ProjectPermissionSub.Certificates + subject(ProjectPermissionSub.Certificates, { + commonName: cert.commonName, + altNames: cert.altNames ?? undefined, + serialNumber: cert.serialNumber + }) ); const { certPrivateKey } = await getCertificateCredentials({ @@ -174,7 +182,11 @@ export const certificateServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateActions.Delete, - ProjectPermissionSub.Certificates + subject(ProjectPermissionSub.Certificates, { + commonName: cert.commonName, + altNames: cert.altNames ?? undefined, + serialNumber: cert.serialNumber + }) ); const deletedCert = await certificateDAL.deleteById(cert.id); @@ -234,7 +246,13 @@ export const certificateServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateActions.Delete, - ProjectPermissionSub.Certificates + subject(ProjectPermissionSub.Certificates, { + commonName: cert.commonName, + altNames: cert.altNames ?? undefined, + serialNumber: cert.serialNumber, + friendlyName: cert.friendlyName, + status: cert.status + }) ); if (cert.status === CertStatus.REVOKED) throw new Error("Certificate already revoked"); @@ -309,7 +327,11 @@ export const certificateServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateActions.Read, - ProjectPermissionSub.Certificates + subject(ProjectPermissionSub.Certificates, { + commonName: cert.commonName, + altNames: cert.altNames ?? undefined, + serialNumber: cert.serialNumber + }) ); const certBody = await certificateBodyDAL.findOne({ certId: cert.id }); @@ -397,7 +419,7 @@ export const certificateServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateActions.Create, + ProjectPermissionCertificateActions.Import, ProjectPermissionSub.Certificates ); @@ -610,11 +632,23 @@ export const certificateServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateActions.Read, - ProjectPermissionSub.Certificates + subject(ProjectPermissionSub.Certificates, { + commonName: cert.commonName, + altNames: cert.altNames ?? undefined, + serialNumber: cert.serialNumber, + friendlyName: cert.friendlyName, + status: cert.status + }) ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateActions.ReadPrivateKey, - ProjectPermissionSub.Certificates + subject(ProjectPermissionSub.Certificates, { + commonName: cert.commonName, + altNames: cert.altNames ?? undefined, + serialNumber: cert.serialNumber, + friendlyName: cert.friendlyName, + status: cert.status + }) ); const certBody = await certificateBodyDAL.findOne({ certId: cert.id }); @@ -726,7 +760,13 @@ export const certificateServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateActions.ReadPrivateKey, - ProjectPermissionSub.Certificates + subject(ProjectPermissionSub.Certificates, { + commonName: cert.commonName, + altNames: cert.altNames ?? undefined, + serialNumber: cert.serialNumber, + friendlyName: cert.friendlyName, + status: cert.status + }) ); // Get certificate bundle (certificate, chain, private key) diff --git a/backend/src/services/pki-sync/pki-sync-service.ts b/backend/src/services/pki-sync/pki-sync-service.ts index 02a76db2a..83d20079a 100644 --- a/backend/src/services/pki-sync/pki-sync-service.ts +++ b/backend/src/services/pki-sync/pki-sync-service.ts @@ -145,9 +145,7 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.Create, - subscriber - ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: subscriber.name }) - : ProjectPermissionSub.PkiSyncs + subject(ProjectPermissionSub.PkiSyncs, { name: subscriber ? subscriber.name : name }) ); // Get the destination app type based on PKI sync destination @@ -236,8 +234,8 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.Edit, currentSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: currentSubscriber.name }) - : ProjectPermissionSub.PkiSyncs + ? subject(ProjectPermissionSub.PkiSyncs, { name: currentSubscriber.name }) + : subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) ); if (name && name !== pkiSync.name) { @@ -332,8 +330,8 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.Delete, pkiSyncSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: pkiSyncSubscriber.name }) - : ProjectPermissionSub.PkiSyncs + ? subject(ProjectPermissionSub.PkiSyncs, { name: pkiSyncSubscriber.name }) + : subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) ); return pkiSyncDAL.deleteById(id); @@ -352,7 +350,7 @@ export const pkiSyncServiceFactory = ({ projectId }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.List, ProjectPermissionSub.PkiSyncs); const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId); @@ -407,8 +405,8 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.Read, findSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: findSubscriber.name }) - : ProjectPermissionSub.PkiSyncs + ? subject(ProjectPermissionSub.PkiSyncs, { name: findSubscriber.name }) + : subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) ); const result = { @@ -443,8 +441,8 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.SyncCertificates, syncSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: syncSubscriber.name }) - : ProjectPermissionSub.PkiSyncs + ? subject(ProjectPermissionSub.PkiSyncs, { name: syncSubscriber.name }) + : subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) ); await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: id }); @@ -484,8 +482,8 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.ImportCertificates, importSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: importSubscriber.name }) - : ProjectPermissionSub.PkiSyncs + ? subject(ProjectPermissionSub.PkiSyncs, { name: importSubscriber.name }) + : subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) ); await pkiSyncQueue.queuePkiSyncImportCertificatesById({ syncId: id }); @@ -517,8 +515,8 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.RemoveCertificates, removeSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: removeSubscriber.name }) - : ProjectPermissionSub.PkiSyncs + ? subject(ProjectPermissionSub.PkiSyncs, { name: removeSubscriber.name }) + : subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) ); await pkiSyncQueue.queuePkiSyncRemoveCertificatesById({ syncId: id }); @@ -549,7 +547,17 @@ export const pkiSyncServiceFactory = ({ projectId: pkiSync.projectId }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Edit, ProjectPermissionSub.PkiSyncs); + let pkiSyncSubscriber; + if (pkiSync.subscriberId) { + pkiSyncSubscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId); + } + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiSyncActions.Edit, + pkiSyncSubscriber + ? subject(ProjectPermissionSub.PkiSyncs, { name: pkiSyncSubscriber.name }) + : subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) + ); await validateCertificatesProjectOwnership(certificateIds, pkiSync.projectId); @@ -588,7 +596,10 @@ export const pkiSyncServiceFactory = ({ projectId: pkiSync.projectId }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Edit, ProjectPermissionSub.PkiSyncs); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiSyncActions.Edit, + subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) + ); const removedCount = await certificateSyncDAL.removeCertificates(pkiSyncId, certificateIds); @@ -626,7 +637,10 @@ export const pkiSyncServiceFactory = ({ projectId: pkiSync.projectId }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiSyncActions.Read, + subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) + ); const result = await certificateSyncDAL.findWithDetails({ pkiSyncId, diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index d30462e9b..9c24171bb 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -20,6 +20,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { ProjectPermissionActions, ProjectPermissionCertificateActions, + ProjectPermissionCertificateAuthorityActions, ProjectPermissionMemberActions, ProjectPermissionPkiSubscriberActions, ProjectPermissionPkiTemplateActions, @@ -911,7 +912,7 @@ export const projectServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionCertificateAuthorityActions.List, ProjectPermissionSub.CertificateAuthorities ); @@ -959,7 +960,7 @@ export const projectServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateActions.Read, + ProjectPermissionCertificateActions.List, ProjectPermissionSub.Certificates ); diff --git a/frontend/src/context/ProjectPermissionContext/index.tsx b/frontend/src/context/ProjectPermissionContext/index.tsx index 415a82641..cf3fe1b15 100644 --- a/frontend/src/context/ProjectPermissionContext/index.tsx +++ b/frontend/src/context/ProjectPermissionContext/index.tsx @@ -4,6 +4,7 @@ export { ProjectPermissionActions, ProjectPermissionAuditLogsActions, ProjectPermissionCertificateActions, + ProjectPermissionCertificateAuthorityActions, ProjectPermissionCertificateProfileActions, ProjectPermissionCmekActions, ProjectPermissionDynamicSecretActions, diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index a35451cd3..d8406ab25 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -12,7 +12,9 @@ export enum ProjectPermissionCertificateActions { Create = "create", Edit = "edit", Delete = "delete", - ReadPrivateKey = "read-private-key" + List = "list", + ReadPrivateKey = "read-private-key", + Import = "import" } export enum ProjectPermissionSecretActions { @@ -67,6 +69,7 @@ export enum ProjectPermissionPkiSyncActions { Create = "create", Edit = "edit", Delete = "delete", + List = "list", SyncCertificates = "sync-certificates", ImportCertificates = "import-certificates", RemoveCertificates = "remove-certificates" @@ -128,13 +131,25 @@ export enum ProjectPermissionPkiTemplateActions { ListCerts = "list-certs" } -export enum ProjectPermissionCertificateProfileActions { +export enum ProjectPermissionCertificateAuthorityActions { Read = "read", Create = "create", Edit = "edit", Delete = "delete", + List = "list", + Renew = "renew", + SignIntermediate = "sign-intermediate" +} + +export enum ProjectPermissionCertificateProfileActions { + Read = "read", + List = "list", + Create = "create", + Edit = "edit", + Delete = "delete", IssueCert = "issue-cert", - RevealAcmeEabSecret = "reveal-acme-eab-secret" + RevealAcmeEabSecret = "reveal-acme-eab-secret", + RotateAcmeEabSecret = "rotate-acme-eab-secret" } export enum ProjectPermissionSecretRotationActions { @@ -230,6 +245,9 @@ export type ConditionalProjectPermissionSubject = | ProjectPermissionSub.SshHosts | ProjectPermissionSub.PkiSubscribers | ProjectPermissionSub.CertificateTemplates + | ProjectPermissionSub.CertificateAuthorities + | ProjectPermissionSub.Certificates + | ProjectPermissionSub.CertificateProfiles | ProjectPermissionSub.SecretFolders | ProjectPermissionSub.SecretImports | ProjectPermissionSub.SecretRotation @@ -361,7 +379,24 @@ export type SecretSyncSubjectFields = { }; export type PkiSyncSubjectFields = { - subscriberId: string; + subscriberName: string; + name: string; +}; + +export type CertificateAuthoritySubjectFields = { + name: string; +}; + +export type CertificateSubjectFields = { + commonName?: string; + altNames?: string; + serialNumber?: string; + friendlyName?: string; + status?: string; +}; + +export type CertificateProfileSubjectFields = { + slug: string; }; export type SecretRotationSubjectFields = { @@ -457,8 +492,21 @@ export type ProjectPermissionSet = | (ForcedSubject & IdentityManagementSubjectFields) ) ] - | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] - | [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates] + | [ + ProjectPermissionCertificateAuthorityActions, + ( + | ProjectPermissionSub.CertificateAuthorities + | (ForcedSubject & + CertificateAuthoritySubjectFields) + ) + ] + | [ + ProjectPermissionCertificateActions, + ( + | ProjectPermissionSub.Certificates + | (ForcedSubject & CertificateSubjectFields) + ) + ] | [ ProjectPermissionPkiTemplateActions, ( @@ -484,7 +532,14 @@ export type ProjectPermissionSet = | (ForcedSubject & PkiSubscriberSubjectFields) ) ] - | [ProjectPermissionCertificateProfileActions, ProjectPermissionSub.CertificateProfiles] + | [ + ProjectPermissionCertificateProfileActions, + ( + | ProjectPermissionSub.CertificateProfiles + | (ForcedSubject & + CertificateProfileSubjectFields) + ) + ] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx index 3e0f95807..3bfd02466 100644 --- a/frontend/src/context/index.tsx +++ b/frontend/src/context/index.tsx @@ -15,6 +15,7 @@ export { ProjectPermissionActions, ProjectPermissionAuditLogsActions, ProjectPermissionCertificateActions, + ProjectPermissionCertificateAuthorityActions, ProjectPermissionCertificateProfileActions, ProjectPermissionCmekActions, ProjectPermissionDynamicSecretActions, diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index 74a1ade31..a11504193 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -72,7 +72,7 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.SIGN_INTERMEDIATE]: "Sign intermediate", [EventType.IMPORT_CA_CERT]: "Import CA certificate", [EventType.GET_CA_CRL]: "Get CA CRL", - [EventType.ISSUE_CERT]: "Issue certificate", + [EventType.ISSUE_CERT]: "Request certificate", [EventType.IMPORT_CERT]: "Import certificate", [EventType.GET_CERT]: "Get certificate", [EventType.DELETE_CERT]: "Delete certificate", @@ -225,7 +225,7 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.UPDATE_PKI_SUBSCRIBER]: "Update PKI subscriber", [EventType.DELETE_PKI_SUBSCRIBER]: "Delete PKI subscriber", [EventType.GET_PKI_SUBSCRIBER]: "Get PKI subscriber", - [EventType.ISSUE_PKI_SUBSCRIBER_CERT]: "Issue PKI subscriber certificate", + [EventType.ISSUE_PKI_SUBSCRIBER_CERT]: "Request PKI subscriber certificate", [EventType.SIGN_PKI_SUBSCRIBER_CERT]: "Sign PKI subscriber certificate", [EventType.AUTOMATED_RENEW_SUBSCRIBER_CERT]: "Automated renew PKI subscriber certificate", [EventType.LIST_PKI_SUBSCRIBER_CERTS]: "List PKI subscriber certificates", @@ -287,7 +287,7 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.DELETE_CERTIFICATE_PROFILE]: "Delete Certificate Profile", [EventType.GET_CERTIFICATE_PROFILE]: "Get Certificate Profile", [EventType.LIST_CERTIFICATE_PROFILES]: "List Certificate Profiles", - [EventType.ISSUE_CERTIFICATE_FROM_PROFILE]: "Issue Certificate From Profile", + [EventType.ISSUE_CERTIFICATE_FROM_PROFILE]: "Request Certificate From Profile", [EventType.SIGN_CERTIFICATE_FROM_PROFILE]: "Sign Certificate From Profile", [EventType.ORDER_CERTIFICATE_FROM_PROFILE]: "Order Certificate From Profile", [EventType.GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE]: diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx index b2cd5e696..22a3ab1d6 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx @@ -1,4 +1,5 @@ import { Helmet } from "react-helmet"; +import { subject } from "@casl/ability"; import { faChevronLeft } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, useNavigate, useParams } from "@tanstack/react-router"; @@ -7,6 +8,7 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { + AccessRestrictedBanner, Button, DeleteActionModal, DropdownMenu, @@ -18,7 +20,7 @@ import { } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { - ProjectPermissionActions, + ProjectPermissionCertificateAuthorityActions, ProjectPermissionSub, useOrganization, useProject @@ -88,63 +90,82 @@ const Page = () => { return (
{data && ( -
- - - Certificate Authorities - - - - -
- - - -
-
- - + {(isAllowed) => + isAllowed ? ( +
+ - {(isAllowed) => ( - handlePopUpOpen("deleteCa")} - disabled={!isAllowed} - > - Delete CA - - )} - - - - -
-
- -
-
- - -
-
-
+ + Certificate Authorities + + + + +
+ + + +
+
+ + + {(canDelete) => ( + handlePopUpOpen("deleteCa")} + disabled={!canDelete} + > + Delete CA + + )} + + +
+
+
+
+ +
+
+ + +
+
+
+ ) : ( +
+ +
+ ) + } + )} diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCertificatesSection/CaCertificatesSection.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCertificatesSection/CaCertificatesSection.tsx index 8309a9e17..4502138e0 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCertificatesSection/CaCertificatesSection.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCertificatesSection/CaCertificatesSection.tsx @@ -2,9 +2,10 @@ import { CaCertificatesTable } from "./CaCertificatesTable"; type Props = { caId: string; + caName: string; }; -export const CaCertificatesSection = ({ caId }: Props) => { +export const CaCertificatesSection = ({ caId, caName }: Props) => { return (
@@ -21,7 +22,7 @@ export const CaCertificatesSection = ({ caId }: Props) => { */}
- +
); diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCertificatesSection/CaCertificatesTable.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCertificatesSection/CaCertificatesTable.tsx index c04b8525a..1146ec0a3 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCertificatesSection/CaCertificatesTable.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCertificatesSection/CaCertificatesTable.tsx @@ -1,3 +1,4 @@ +import { subject } from "@casl/ability"; import { faCertificate, faEllipsis } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as x509 from "@peculiar/x509"; @@ -22,14 +23,15 @@ import { Tr } from "@app/components/v2"; import { Badge } from "@app/components/v3"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { ProjectPermissionCertificateAuthorityActions, ProjectPermissionSub } from "@app/context"; import { useGetCaCerts } from "@app/hooks/api"; type Props = { caId: string; + caName: string; }; -export const CaCertificatesTable = ({ caId }: Props) => { +export const CaCertificatesTable = ({ caId, caName }: Props) => { const { data: caCerts, isPending } = useGetCaCerts(caId); const downloadTxtFile = (filename: string, content: string) => { @@ -77,8 +79,10 @@ export const CaCertificatesTable = ({ caId }: Props) => { {(isAllowed) => ( { )} {(isAllowed) => ( {

CA Details

- + {(isAllowed) => { return ( @@ -154,8 +158,8 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
{ca.status === CaStatus.ACTIVE && ( {(isAllowed) => { return ( @@ -190,8 +194,8 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => { )} {ca.status === CaStatus.PENDING_CERTIFICATE && ( {(isAllowed) => { return ( diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx index 7e121eec9..145248bae 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx @@ -4,7 +4,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; +import { + ProjectPermissionCertificateAuthorityActions, + ProjectPermissionSub, + useProject +} from "@app/context"; import { CaStatus, CaType, useDeleteCa, useUpdateCa } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -57,7 +61,7 @@ export const CaSection = () => {

Internal Certificate Authorities

{(isAllowed) => ( diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx index 27de9315a..06cb97532 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx @@ -1,3 +1,4 @@ +import { subject } from "@casl/ability"; import { faBan, faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate } from "@tanstack/react-router"; @@ -23,10 +24,11 @@ import { } from "@app/components/v2"; import { Badge } from "@app/components/v3"; import { - ProjectPermissionActions, + ProjectPermissionCertificateAuthorityActions, ProjectPermissionSub, useOrganization, - useProject + useProject, + useProjectPermission } from "@app/context"; import { CaStatus, CaType, useListCasByTypeAndProjectId } from "@app/hooks/api"; import { @@ -53,6 +55,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { const navigate = useNavigate(); const { currentOrg } = useOrganization(); const { currentProject } = useProject(); + const { permission } = useProjectPermission(); const { data, isPending } = useListCasByTypeAndProjectId(CaType.INTERNAL, currentProject.id); const cas = data as TInternalCertificateAuthority[]; @@ -75,11 +78,24 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { cas && cas.length > 0 && cas.map((ca) => { + const canReadCa = permission.can( + ProjectPermissionCertificateAuthorityActions.Read, + subject(ProjectPermissionSub.CertificateAuthorities, { + caId: ca.id, + name: ca.name + }) + ); + return ( + canReadCa && navigate({ to: "/organizations/$orgId/projects/cert-management/$projectId/ca/$caId", params: { @@ -118,8 +134,11 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { {ca.status === CaStatus.PENDING_CERTIFICATE && ( {(isAllowed) => ( { )} {ca.status !== CaStatus.PENDING_CERTIFICATE && ( {(isAllowed) => ( { )} {(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && ( {(isAllowed) => ( { )} {(isAllowed) => ( {

External Certificate Authorities

{(isAllowed) => ( diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx index 017bd4506..6d2b56f3f 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx @@ -1,3 +1,4 @@ +import { subject } from "@casl/ability"; import { faBan, faCertificate, @@ -26,7 +27,12 @@ import { Tr } from "@app/components/v2"; import { Badge } from "@app/components/v3"; -import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; +import { + ProjectPermissionCertificateAuthorityActions, + ProjectPermissionSub, + useProject, + useProjectPermission +} from "@app/context"; import { CaStatus, CaType, useListExternalCasByProjectId } from "@app/hooks/api"; import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -45,6 +51,7 @@ type Props = { export const ExternalCaTable = ({ handlePopUpOpen }: Props) => { const { currentProject } = useProject(); + const { permission } = useProjectPermission(); const { data, isPending } = useListExternalCasByProjectId(currentProject.id); return ( @@ -65,17 +72,30 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => { data && data.length > 0 && data.map((ca) => { + const canReadCa = permission.can( + ProjectPermissionCertificateAuthorityActions.Read, + subject(ProjectPermissionSub.CertificateAuthorities, { + caId: ca.id, + name: ca.name + }) + ); + return ( { + onClick={() => + canReadCa && handlePopUpOpen("ca", { caId: ca.id, name: ca.name, type: ca.type - }); - }} + }) + } > {ca.name} {ca.type} @@ -95,8 +115,11 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => { {(isAllowed) => ( { {(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && ( {(isAllowed) => ( { )} {(isAllowed) => ( { if (certificateDetails) return "Certificate Created Successfully"; if (cert) return "Certificate Details"; - return "Issue New Certificate"; + return "Request New Certificate"; }; const getModalSubTitle = () => { if (certificateDetails) return "Certificate has been successfully created and is ready for use"; if (cert) return "View certificate information"; - return "Issue a new certificate using a certificate profile"; + return "Request a new certificate using a certificate profile"; }; return ( @@ -498,7 +498,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId } isLoading={isSubmitting} isDisabled={isSubmitting || (!actualSelectedProfile && !profileId)} > - {cert ? "Update" : "Issue Certificate"} + {cert ? "Update" : "Request Certificate"}
- - - - - - - - - - - } - onClick={(e) => { - e.stopPropagation(); - handleCopyId(); - }} - > - Copy Sync ID - - - {(isAllowed: boolean) => ( - } - onClick={(e) => { - e.stopPropagation(); - onTriggerSyncCertificates(pkiSync); - }} - isDisabled={!isAllowed} - > - -
- Trigger Sync - -
+
+

{name}

+ {description && ( + + - + )} +
+

+ {destinationDetails.name} +

+
+ + + +
+ {syncStatus && ( + + {lastSyncedAt && ( +
+
+ +
Last Synced
+
+
+ {format(new Date(lastSyncedAt), "yyyy-MM-dd, hh:mm aaa")} +
+
+ )} + {failureMessage && ( +
+
+ +
Failure Reason
+
+
+ {failureMessage} +
+
+ )} +
+ ) : undefined + } + > +
+ +
+ )} -
- {syncOption?.canImportCertificates && ( - - {(isAllowed: boolean) => ( + {!isAutoSyncEnabled && ( + + + + {!syncStatus && "Auto-Sync Disabled"} + + + )} + {syncOption?.canImportCertificates && ( + + )} + +
+ + + + + + + + + + } + icon={} onClick={(e) => { e.stopPropagation(); - onTriggerImportCertificates(pkiSync); + handleCopyId(); }} - isDisabled={!isAllowed} > - -
- Import Certificates - -
-
+ Copy Sync ID
- )} -
- )} - - {(isAllowed: boolean) => ( - } - onClick={(e) => { - e.stopPropagation(); - onTriggerRemoveCertificates(pkiSync); - }} - isDisabled={!isAllowed} - > - -
- Remove Certificates - -
-
-
- )} -
- - {(isAllowed: boolean) => ( - } - onClick={(e) => { - e.stopPropagation(); - onToggleEnable(pkiSync); - }} - > - {isAutoSyncEnabled ? "Disable" : "Enable"} Auto-Sync - - )} - - - {(isAllowed: boolean) => ( - } - onClick={(e) => { - e.stopPropagation(); - onDelete(pkiSync); - }} - > - Delete Sync - - )} - -
- - - - + {(allowed: boolean) => ( + } + onClick={(e) => { + e.stopPropagation(); + onTriggerSyncCertificates(pkiSync); + }} + isDisabled={!allowed} + > + +
+ Trigger Sync + +
+
+
+ )} + + {syncOption?.canImportCertificates && ( + + {(allowed: boolean) => ( + } + onClick={(e) => { + e.stopPropagation(); + onTriggerImportCertificates(pkiSync); + }} + isDisabled={!allowed} + > + +
+ Import Certificates + +
+
+
+ )} +
+ )} + + {(allowed: boolean) => ( + } + onClick={(e) => { + e.stopPropagation(); + onTriggerRemoveCertificates(pkiSync); + }} + isDisabled={!allowed} + > + +
+ Remove Certificates + +
+
+
+ )} +
+ + {(allowed: boolean) => ( + + } + onClick={(e) => { + e.stopPropagation(); + onToggleEnable(pkiSync); + }} + > + {isAutoSyncEnabled ? "Disable" : "Enable"} Auto-Sync + + )} + + + {(allowed: boolean) => ( + } + onClick={(e) => { + e.stopPropagation(); + onDelete(pkiSync); + }} + > + Delete Sync + + )} + + + + + + + ); + }} + ); }; diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx index 8277d3345..f87f8cdba 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx @@ -112,7 +112,9 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => { }, [updatePkiSyncMutation, id, projectId, pkiSync.isAutoSyncEnabled]); const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { - subscriberId: subscriberId || "" + subscriberId: subscriberId || "", + subscriberName: destinationName, + name: pkiSync.name }); return ( diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncCertificatesSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncCertificatesSection.tsx index b453c4d79..8a990bd4c 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncCertificatesSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncCertificatesSection.tsx @@ -32,6 +32,7 @@ import { import { Badge } from "@app/components/v3"; import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types"; +import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs"; import { useListPkiSyncCertificates, useRemoveCertificatesFromPkiSync } from "@app/hooks/api"; import { CertificateSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs"; @@ -84,8 +85,12 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => { const totalCount = data?.totalCount || 0; const removeCertificatesFromSync = useRemoveCertificatesFromPkiSync(); + const destinationName = PKI_SYNC_MAP[pkiSync.destination].name; + const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { - subscriberId: pkiSync.subscriberId || "" + subscriberId: pkiSync.subscriberId || "", + subscriberName: destinationName, + name: pkiSync.name }); const handleRemoveCertificate = async (certificateId: string) => { diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection.tsx index c59d2df6d..22611171b 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection.tsx @@ -55,7 +55,9 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props) } const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { - subscriberId: subscriberId || "" + subscriberId: subscriberId || "", + subscriberName: destinationDetails.name, + name: pkiSync.name }); return ( diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDetailsSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDetailsSection.tsx index a3d95d5c3..45fc9f371 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDetailsSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDetailsSection.tsx @@ -10,6 +10,7 @@ import { PkiSyncStatusBadge } from "@app/components/pki-syncs"; import { IconButton } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types"; +import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs"; import { PkiSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs"; const GenericFieldLabel = ({ @@ -50,8 +51,12 @@ export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => { return null; }, [syncStatus, lastSyncMessage]); + const destinationName = PKI_SYNC_MAP[pkiSync.destination].name; + const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { - subscriberId: subscriber?.id || subscriberId || "" + subscriberId: subscriber?.id || subscriberId || "", + subscriberName: destinationName, + name: pkiSync.name }); return ( diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncFieldMappingsSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncFieldMappingsSection.tsx index 66f0ea076..c293f4982 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncFieldMappingsSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncFieldMappingsSection.tsx @@ -7,6 +7,7 @@ import { IconButton } from "@app/components/v2"; import { Badge } from "@app/components/v3"; import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types"; +import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs"; import { PkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs"; const GenericFieldLabel = ({ @@ -35,9 +36,12 @@ export const PkiSyncFieldMappingsSection = ({ pkiSync, onEditMappings }: Props) } const fieldMappings = pkiSync.syncOptions?.fieldMappings; + const destinationName = PKI_SYNC_MAP[pkiSync.destination].name; const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { - subscriberId: pkiSync.subscriberId || "" + subscriberId: pkiSync.subscriberId || "", + subscriberName: destinationName, + name: pkiSync.name }); return ( diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncOptionsSection/PkiSyncOptionsSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncOptionsSection/PkiSyncOptionsSection.tsx index e9a509ebb..8cf57b874 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncOptionsSection/PkiSyncOptionsSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncOptionsSection/PkiSyncOptionsSection.tsx @@ -7,6 +7,7 @@ import { IconButton } from "@app/components/v2"; import { Badge } from "@app/components/v3"; import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types"; +import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs"; import { TPkiSync } from "@app/hooks/api/pkiSyncs"; const GenericFieldLabel = ({ @@ -34,8 +35,12 @@ export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => { syncOptions: { canRemoveCertificates } } = pkiSync; + const destinationName = PKI_SYNC_MAP[pkiSync.destination].name; + const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { - subscriberId: pkiSync.subscriberId || "" + subscriberId: pkiSync.subscriberId || "", + subscriberName: destinationName, + name: pkiSync.name }); return ( diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncSourceSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncSourceSection.tsx index c9dc041df..3097dd4c8 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncSourceSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncSourceSection.tsx @@ -9,6 +9,7 @@ import { IconButton, Tooltip } from "@app/components/v2"; import { Badge } from "@app/components/v3"; import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types"; +import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs"; import { TPkiSync } from "@app/hooks/api/pkiSyncs"; const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => ( @@ -26,8 +27,12 @@ type Props = { export const PkiSyncSourceSection = ({ pkiSync, onEditSource }: Props) => { const { subscriberId, subscriber } = pkiSync; + const destinationName = PKI_SYNC_MAP[pkiSync.destination].name; + const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { - subscriberId: subscriberId || "" + subscriberId: subscriberId || "", + subscriberName: destinationName, + name: pkiSync.name }); return ( diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx index 39bb3c4e9..9807cb98f 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx @@ -4,10 +4,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal } from "@app/components/v2"; -import { useProjectPermission } from "@app/context"; import { - ProjectPermissionActions, + ProjectPermissionCertificateProfileActions, ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types"; import { usePopUp } from "@app/hooks"; @@ -21,8 +21,6 @@ import { ProfileList } from "./ProfileList"; import { RevealAcmeEabSecretModal } from "./RevealAcmeEabSecretModal"; export const CertificateProfilesTab = () => { - const { permission } = useProjectPermission(); - const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -35,11 +33,6 @@ export const CertificateProfilesTab = () => { const deleteProfile = useDeleteCertificateProfile(); - const canCreateProfile = permission.can( - ProjectPermissionActions.Create, - ProjectPermissionSub.CertificateAuthorities - ); - const handleCreateProfile = () => { setIsCreateModalOpen(true); }; @@ -84,16 +77,22 @@ export const CertificateProfilesTab = () => {

- {canCreateProfile && ( - - )} + + {(isAllowed) => ( + + )} + { - const { permission } = useProjectPermission(); - const { data: caData } = useGetInternalCaById(profile.caId ?? ""); const { popUp, handlePopUpToggle } = usePopUp(["issueCertificate"] as const); @@ -71,26 +69,6 @@ export const ProfileRow = ({ templateId: profile.certificateTemplateId }); - const canEditProfile = permission.can( - ProjectPermissionActions.Edit, - ProjectPermissionSub.CertificateAuthorities - ); - - const canRevealProfileAcmeEabSecret = permission.can( - ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret, - ProjectPermissionSub.CertificateProfiles - ); - - const canIssueCertificate = permission.can( - ProjectPermissionCertificateProfileActions.IssueCert, - ProjectPermissionSub.CertificateProfiles - ); - - const canDeleteProfile = permission.can( - ProjectPermissionActions.Delete, - ProjectPermissionSub.CertificateAuthorities - ); - const getEnrollmentTypeBadge = (enrollmentType: string) => { const config = { api: { variant: "ghost" as const, label: "API" }, @@ -149,50 +127,82 @@ export const ProfileRow = ({ > Copy Profile ID - {canEditProfile && ( - { - e.stopPropagation(); - onEditProfile(profile); - }} - icon={} + + {(isAllowed) => + isAllowed && ( + { + e.stopPropagation(); + onEditProfile(profile); + }} + icon={} + > + Edit Profile + + ) + } + + {profile.enrollmentType === "acme" && ( + - Edit Profile - + {(isAllowed) => + isAllowed && ( + { + e.stopPropagation(); + onRevealProfileAcmeEabSecret(profile); + }} + icon={} + > + Reveal ACME EAB + + ) + } + )} - {canRevealProfileAcmeEabSecret && profile.enrollmentType === "acme" && ( - { - e.stopPropagation(); - onRevealProfileAcmeEabSecret(profile); - }} - icon={} + {profile.enrollmentType === "api" && ( + - Reveal ACME EAB - - )} - {canIssueCertificate && profile.enrollmentType === "api" && ( - { - e.stopPropagation(); - handlePopUpToggle("issueCertificate"); - }} - icon={} - > - Issue Certificate - - )} - {canDeleteProfile && ( - { - e.stopPropagation(); - onDeleteProfile(profile); - }} - icon={} - > - Delete Profile - + {(isAllowed) => + isAllowed && ( + { + e.stopPropagation(); + handlePopUpToggle("issueCertificate"); + }} + icon={} + > + Request Certificate + + ) + } + )} + + {(isAllowed) => + isAllowed && ( + { + e.stopPropagation(); + onDeleteProfile(profile); + }} + icon={} + > + Delete Profile + + ) + } + { - const { permission } = useProjectPermission(); - const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -26,11 +24,6 @@ export const CertificateTemplatesV2Tab = () => { const deleteTemplateV2 = useDeleteCertificateTemplateV2WithPolicies(); - const canCreateTemplate = permission.can( - ProjectPermissionPkiTemplateActions.Create, - ProjectPermissionSub.CertificateTemplates - ); - const handleCreateTemplate = () => { setIsCreateModalOpen(true); }; @@ -70,16 +63,22 @@ export const CertificateTemplatesV2Tab = () => {

- {canCreateTemplate && ( - - )} + + {(isAllowed) => ( + + )} + diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/TemplateList.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/TemplateList.tsx index 108fe966c..a2aa5f78c 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/TemplateList.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/TemplateList.tsx @@ -1,6 +1,17 @@ -import { faCircleInfo, faEdit, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { useCallback } from "react"; +import { subject } from "@casl/ability"; +import { + faCheck, + faCircleInfo, + faCopy, + faEdit, + faEllipsis, + faTrash +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; import { DropdownMenu, DropdownMenuContent, @@ -17,11 +28,12 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { useProject, useProjectPermission } from "@app/context"; +import { useProject } from "@app/context"; import { ProjectPermissionPkiTemplateActions, ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types"; +import { useToggle } from "@app/hooks"; import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries"; import { TCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/types"; @@ -31,8 +43,8 @@ interface Props { } export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => { - const { permission } = useProjectPermission(); const { currentProject } = useProject(); + const [isIdCopied, setIsIdCopied] = useToggle(false); const { data, isLoading } = useListCertificateTemplatesV2({ projectId: currentProject?.id || "", @@ -42,20 +54,25 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => { const templates = data?.certificateTemplates || []; + const handleCopyId = useCallback( + (templateId: string) => { + setIsIdCopied.on(); + navigator.clipboard.writeText(templateId); + + createNotification({ + text: "Template ID copied to clipboard", + type: "info" + }); + + setTimeout(() => setIsIdCopied.off(), 2000); + }, + [setIsIdCopied] + ); + if (!currentProject?.id) { return null; } - const canEditTemplate = permission.can( - ProjectPermissionPkiTemplateActions.Edit, - ProjectPermissionSub.CertificateTemplates - ); - - const canDeleteTemplate = permission.can( - ProjectPermissionPkiTemplateActions.Delete, - ProjectPermissionSub.CertificateTemplates - ); - const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString(); }; @@ -110,28 +127,55 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => { - {canEditTemplate && ( - { - e.stopPropagation(); - onEditTemplate(template); - }} - icon={} - > - Edit Template - - )} - {canDeleteTemplate && ( - { - e.stopPropagation(); - onDeleteTemplate(template); - }} - icon={} - > - Delete Template - - )} + { + e.stopPropagation(); + handleCopyId(template.id); + }} + icon={} + > + Copy Template ID + + + {(isAllowed) => + isAllowed && ( + { + e.stopPropagation(); + onEditTemplate(template); + }} + icon={} + > + Edit Template + + ) + } + + + {(isAllowed) => + isAllowed && ( + { + e.stopPropagation(); + onDeleteTemplate(template); + }} + icon={} + > + Delete Template + + ) + } + diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/CertificateAuthorityPermissionConditions.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/CertificateAuthorityPermissionConditions.tsx new file mode 100644 index 000000000..3e853fc4a --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/CertificateAuthorityPermissionConditions.tsx @@ -0,0 +1,19 @@ +import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types"; + +import { ConditionsFields } from "./ConditionsFields"; + +type Props = { + position?: number; + isDisabled?: boolean; +}; + +export const CertificateAuthorityPermissionConditions = ({ position = 0, isDisabled }: Props) => { + return ( + + ); +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/CertificatePermissionConditions.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/CertificatePermissionConditions.tsx new file mode 100644 index 000000000..06eb5a8f9 --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/CertificatePermissionConditions.tsx @@ -0,0 +1,24 @@ +import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types"; + +import { ConditionsFields } from "./ConditionsFields"; + +type Props = { + position?: number; + isDisabled?: boolean; +}; + +export const CertificatePermissionConditions = ({ position = 0, isDisabled }: Props) => { + return ( + + ); +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/CertificateProfilePermissionConditions.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/CertificateProfilePermissionConditions.tsx new file mode 100644 index 000000000..0dfaeea70 --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/CertificateProfilePermissionConditions.tsx @@ -0,0 +1,19 @@ +import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types"; + +import { ConditionsFields } from "./ConditionsFields"; + +type Props = { + position?: number; + isDisabled?: boolean; +}; + +export const CertificateProfilePermissionConditions = ({ position = 0, isDisabled }: Props) => { + return ( + + ); +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiSyncPermissionConditions.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiSyncPermissionConditions.tsx index a045ca258..254629515 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiSyncPermissionConditions.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiSyncPermissionConditions.tsx @@ -13,7 +13,7 @@ export const PkiSyncPermissionConditions = ({ position = 0, isDisabled }: Props) isDisabled={isDisabled} subject={ProjectPermissionSub.PkiSyncs} position={position} - selectOptions={[{ value: "subscriberName", label: "Subscriber Name" }]} + selectOptions={[{ value: "name", label: "Sync Name" }]} /> ); }; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index a8e5d6fda..1178102f2 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -7,6 +7,7 @@ import { Tooltip } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionCertificateActions, + ProjectPermissionCertificateAuthorityActions, ProjectPermissionCertificateProfileActions, ProjectPermissionCmekActions, ProjectPermissionSub @@ -56,7 +57,19 @@ const CertificatePolicyActionSchema = z.object({ [ProjectPermissionCertificateActions.Delete]: z.boolean().optional(), [ProjectPermissionCertificateActions.Edit]: z.boolean().optional(), [ProjectPermissionCertificateActions.Read]: z.boolean().optional(), - [ProjectPermissionCertificateActions.ReadPrivateKey]: z.boolean().optional() + [ProjectPermissionCertificateActions.List]: z.boolean().optional(), + [ProjectPermissionCertificateActions.ReadPrivateKey]: z.boolean().optional(), + [ProjectPermissionCertificateActions.Import]: z.boolean().optional() +}); + +const CertificateAuthorityPolicyActionSchema = z.object({ + [ProjectPermissionCertificateAuthorityActions.Create]: z.boolean().optional(), + [ProjectPermissionCertificateAuthorityActions.Delete]: z.boolean().optional(), + [ProjectPermissionCertificateAuthorityActions.Edit]: z.boolean().optional(), + [ProjectPermissionCertificateAuthorityActions.Read]: z.boolean().optional(), + [ProjectPermissionCertificateAuthorityActions.List]: z.boolean().optional(), + [ProjectPermissionCertificateAuthorityActions.Renew]: z.boolean().optional(), + [ProjectPermissionCertificateAuthorityActions.SignIntermediate]: z.boolean().optional() }); const SecretPolicyActionSchema = z.object({ @@ -110,6 +123,7 @@ const PkiSyncPolicyActionSchema = z.object({ [ProjectPermissionPkiSyncActions.Create]: z.boolean().optional(), [ProjectPermissionPkiSyncActions.Edit]: z.boolean().optional(), [ProjectPermissionPkiSyncActions.Delete]: z.boolean().optional(), + [ProjectPermissionPkiSyncActions.List]: z.boolean().optional(), [ProjectPermissionPkiSyncActions.SyncCertificates]: z.boolean().optional(), [ProjectPermissionPkiSyncActions.ImportCertificates]: z.boolean().optional(), [ProjectPermissionPkiSyncActions.RemoveCertificates]: z.boolean().optional() @@ -208,7 +222,6 @@ const PkiSubscriberPolicyActionSchema = z.object({ [ProjectPermissionPkiSubscriberActions.Create]: z.boolean().optional(), [ProjectPermissionPkiSubscriberActions.Edit]: z.boolean().optional(), [ProjectPermissionPkiSubscriberActions.Delete]: z.boolean().optional(), - [ProjectPermissionPkiSubscriberActions.IssueCert]: z.boolean().optional(), [ProjectPermissionPkiSubscriberActions.ListCerts]: z.boolean().optional() }); @@ -217,15 +230,17 @@ const PkiTemplatePolicyActionSchema = z.object({ [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 CertificateProfilePolicyActionSchema = z.object({ [ProjectPermissionCertificateProfileActions.Read]: z.boolean().optional(), + [ProjectPermissionCertificateProfileActions.List]: z.boolean().optional(), [ProjectPermissionCertificateProfileActions.Create]: z.boolean().optional(), [ProjectPermissionCertificateProfileActions.Edit]: z.boolean().optional(), [ProjectPermissionCertificateProfileActions.Delete]: z.boolean().optional(), - [ProjectPermissionCertificateProfileActions.IssueCert]: z.boolean().optional() + [ProjectPermissionCertificateProfileActions.IssueCert]: z.boolean().optional(), + [ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret]: z.boolean().optional(), + [ProjectPermissionCertificateProfileActions.RotateAcmeEabSecret]: z.boolean().optional() }); const SecretEventsPolicyActionSchema = z.object({ @@ -381,8 +396,18 @@ export const projectRoleFormSchema = z.object({ [ProjectPermissionSub.Environments]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.AuditLogs]: AuditLogsPolicyActionSchema.array().default([]), [ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]), - [ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]), - [ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.array().default([]), + [ProjectPermissionSub.CertificateAuthorities]: CertificateAuthorityPolicyActionSchema.extend({ + inverted: z.boolean().optional(), + conditions: ConditionSchema + }) + .array() + .default([]), + [ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.extend({ + inverted: z.boolean().optional(), + conditions: ConditionSchema + }) + .array() + .default([]), [ProjectPermissionSub.PkiSubscribers]: PkiSubscriberPolicyActionSchema.extend({ inverted: z.boolean().optional(), conditions: ConditionSchema @@ -397,8 +422,12 @@ export const projectRoleFormSchema = z.object({ }) .array() .default([]), - [ProjectPermissionSub.CertificateProfiles]: - CertificateProfilePolicyActionSchema.array().default([]), + [ProjectPermissionSub.CertificateProfiles]: CertificateProfilePolicyActionSchema.extend({ + inverted: z.boolean().optional(), + conditions: ConditionSchema + }) + .array() + .default([]), [ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default( [] ), @@ -458,6 +487,9 @@ type TConditionalFields = | ProjectPermissionSub.DynamicSecrets | ProjectPermissionSub.PkiSubscribers | ProjectPermissionSub.CertificateTemplates + | ProjectPermissionSub.CertificateAuthorities + | ProjectPermissionSub.Certificates + | ProjectPermissionSub.CertificateProfiles | ProjectPermissionSub.SshHosts | ProjectPermissionSub.SecretRotation | ProjectPermissionSub.Identity @@ -479,6 +511,9 @@ export const isConditionalSubjects = ( subject === ProjectPermissionSub.SecretRotation || subject === ProjectPermissionSub.PkiSubscribers || subject === ProjectPermissionSub.CertificateTemplates || + subject === ProjectPermissionSub.CertificateAuthorities || + subject === ProjectPermissionSub.Certificates || + subject === ProjectPermissionSub.CertificateProfiles || subject === ProjectPermissionSub.SecretSyncs || subject === ProjectPermissionSub.PkiSyncs || subject === ProjectPermissionSub.SecretEvents || @@ -661,6 +696,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { const canEdit = action.includes(ProjectPermissionPkiSyncActions.Edit); const canDelete = action.includes(ProjectPermissionPkiSyncActions.Delete); const canCreate = action.includes(ProjectPermissionPkiSyncActions.Create); + const canList = action.includes(ProjectPermissionPkiSyncActions.List); const canSyncCertificates = action.includes( ProjectPermissionPkiSyncActions.SyncCertificates ); @@ -679,6 +715,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { [ProjectPermissionPkiSyncActions.Create]: canCreate, [ProjectPermissionPkiSyncActions.Edit]: canEdit, [ProjectPermissionPkiSyncActions.Delete]: canDelete, + [ProjectPermissionPkiSyncActions.List]: canList, [ProjectPermissionPkiSyncActions.SyncCertificates]: canSyncCertificates, [ProjectPermissionPkiSyncActions.ImportCertificates]: canImportCertificates, [ProjectPermissionPkiSyncActions.RemoveCertificates]: canRemoveCertificates, @@ -822,6 +859,33 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { return; } + if (subject === ProjectPermissionSub.CertificateAuthorities) { + const canRead = action.includes(ProjectPermissionCertificateAuthorityActions.Read); + const canCreate = action.includes(ProjectPermissionCertificateAuthorityActions.Create); + const canEdit = action.includes(ProjectPermissionCertificateAuthorityActions.Edit); + const canDelete = action.includes(ProjectPermissionCertificateAuthorityActions.Delete); + const canList = action.includes(ProjectPermissionCertificateAuthorityActions.List); + const canRenew = action.includes(ProjectPermissionCertificateAuthorityActions.Renew); + const canSignIntermediate = action.includes( + ProjectPermissionCertificateAuthorityActions.SignIntermediate + ); + + // from above statement we are sure it won't be undefined + formVal[subject]!.push({ + [ProjectPermissionCertificateAuthorityActions.Read]: canRead, + [ProjectPermissionCertificateAuthorityActions.Create]: canCreate, + [ProjectPermissionCertificateAuthorityActions.Edit]: canEdit, + [ProjectPermissionCertificateAuthorityActions.Delete]: canDelete, + [ProjectPermissionCertificateAuthorityActions.List]: canList, + [ProjectPermissionCertificateAuthorityActions.Renew]: canRenew, + [ProjectPermissionCertificateAuthorityActions.SignIntermediate]: canSignIntermediate, + conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], + inverted + }); + + return; + } + // for other subjects const canRead = action.includes(ProjectPermissionActions.Read); const canEdit = action.includes(ProjectPermissionActions.Edit); @@ -873,17 +937,24 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { const canEdit = action.includes(ProjectPermissionCertificateActions.Edit); const canDelete = action.includes(ProjectPermissionCertificateActions.Delete); const canCreate = action.includes(ProjectPermissionCertificateActions.Create); + const canList = action.includes(ProjectPermissionCertificateActions.List); + const canImport = action.includes(ProjectPermissionCertificateActions.Import); const canReadPrivateKey = action.includes(ProjectPermissionCertificateActions.ReadPrivateKey); - if (!formVal[subject]) formVal[subject] = [{}]; + if (!formVal[subject]) formVal[subject] = []; // from above statement we are sure it won't be undefined - if (canRead) formVal[subject]![0].read = true; - if (canEdit) formVal[subject]![0].edit = true; - if (canCreate) formVal[subject]![0].create = true; - if (canDelete) formVal[subject]![0].delete = true; - if (canReadPrivateKey) - formVal[subject]![0][ProjectPermissionCertificateActions.ReadPrivateKey] = true; + formVal[subject]!.push({ + [ProjectPermissionCertificateActions.Read]: canRead, + [ProjectPermissionCertificateActions.Edit]: canEdit, + [ProjectPermissionCertificateActions.Create]: canCreate, + [ProjectPermissionCertificateActions.Delete]: canDelete, + [ProjectPermissionCertificateActions.List]: canList, + [ProjectPermissionCertificateActions.ReadPrivateKey]: canReadPrivateKey, + [ProjectPermissionCertificateActions.Import]: canImport, + conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], + inverted + }); return; } @@ -1129,9 +1200,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { [ProjectPermissionPkiSubscriberActions.Read]: action.includes( ProjectPermissionPkiSubscriberActions.Read ), - [ProjectPermissionPkiSubscriberActions.IssueCert]: action.includes( - ProjectPermissionPkiSubscriberActions.IssueCert - ), [ProjectPermissionPkiSubscriberActions.ListCerts]: action.includes( ProjectPermissionPkiSubscriberActions.ListCerts ), @@ -1157,9 +1225,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { [ProjectPermissionPkiTemplateActions.Read]: action.includes( ProjectPermissionPkiTemplateActions.Read ), - [ProjectPermissionPkiTemplateActions.IssueCert]: action.includes( - ProjectPermissionPkiTemplateActions.IssueCert - ), [ProjectPermissionPkiTemplateActions.ListCerts]: action.includes( ProjectPermissionPkiTemplateActions.ListCerts ), @@ -1185,9 +1250,20 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { [ProjectPermissionCertificateProfileActions.Read]: action.includes( ProjectPermissionCertificateProfileActions.Read ), + [ProjectPermissionCertificateProfileActions.List]: action.includes( + ProjectPermissionCertificateProfileActions.List + ), [ProjectPermissionCertificateProfileActions.IssueCert]: action.includes( ProjectPermissionCertificateProfileActions.IssueCert - ) + ), + [ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret]: action.includes( + ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret + ), + [ProjectPermissionCertificateProfileActions.RotateAcmeEabSecret]: action.includes( + ProjectPermissionCertificateProfileActions.RotateAcmeEabSecret + ), + conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], + inverted }); return; @@ -1196,7 +1272,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { if (subject === ProjectPermissionSub.PamAccounts) { if (!formVal[subject]) formVal[subject] = []; - formVal[subject].push({ + formVal[subject]!.push({ [ProjectPermissionPamAccountActions.Access]: action.includes( ProjectPermissionPamAccountActions.Access ), @@ -1552,18 +1628,26 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { [ProjectPermissionSub.CertificateAuthorities]: { title: "Certificate Authorities", actions: [ - { label: "Read", value: "read" }, - { label: "Create", value: "create" }, - { label: "Modify", value: "edit" }, - { label: "Remove", value: "delete" } + { label: "Read", value: ProjectPermissionCertificateAuthorityActions.Read }, + { label: "List", value: ProjectPermissionCertificateAuthorityActions.List }, + { label: "Create", value: ProjectPermissionCertificateAuthorityActions.Create }, + { label: "Modify", value: ProjectPermissionCertificateAuthorityActions.Edit }, + { label: "Remove", value: ProjectPermissionCertificateAuthorityActions.Delete }, + { label: "Renew", value: ProjectPermissionCertificateAuthorityActions.Renew }, + { + label: "Sign Intermediate", + value: ProjectPermissionCertificateAuthorityActions.SignIntermediate + } ] }, [ProjectPermissionSub.Certificates]: { title: "Certificates", actions: [ { label: "Read", value: ProjectPermissionCertificateActions.Read }, + { label: "List", value: ProjectPermissionCertificateActions.List }, { label: "Read Private Key", value: ProjectPermissionCertificateActions.ReadPrivateKey }, - { label: "Create", value: ProjectPermissionCertificateActions.Create }, + // { label: "Create", value: ProjectPermissionCertificateActions.Create }, // Hidden from UI - kept for backend compatibility + { label: "Import", value: ProjectPermissionCertificateActions.Import }, { label: "Modify", value: ProjectPermissionCertificateActions.Edit }, { label: "Remove", value: ProjectPermissionCertificateActions.Delete } ] @@ -1575,7 +1659,6 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { { 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 } ] }, @@ -1583,10 +1666,11 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { title: "Certificate Profiles", actions: [ { label: "Read", value: ProjectPermissionCertificateProfileActions.Read }, + { label: "List", value: ProjectPermissionCertificateProfileActions.List }, { label: "Create", value: ProjectPermissionCertificateProfileActions.Create }, { label: "Modify", value: ProjectPermissionCertificateProfileActions.Edit }, { label: "Remove", value: ProjectPermissionCertificateProfileActions.Delete }, - { label: "Issue Certificates", value: ProjectPermissionCertificateProfileActions.IssueCert } + { label: "Request Certificates", value: ProjectPermissionCertificateProfileActions.IssueCert } ] }, [ProjectPermissionSub.SshCertificateAuthorities]: { @@ -1642,7 +1726,6 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { { label: "Create", value: ProjectPermissionPkiSubscriberActions.Create }, { label: "Modify", value: ProjectPermissionPkiSubscriberActions.Edit }, { label: "Remove", value: ProjectPermissionPkiSubscriberActions.Delete }, - { label: "Issue Certificate", value: ProjectPermissionPkiSubscriberActions.IssueCert }, { label: "List Certificates", value: ProjectPermissionPkiSubscriberActions.ListCerts } ] }, @@ -1716,6 +1799,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { title: "Certificate Syncs", actions: [ { label: "Read", value: ProjectPermissionPkiSyncActions.Read }, + { label: "List", value: ProjectPermissionPkiSyncActions.List }, { label: "Create", value: ProjectPermissionPkiSyncActions.Create }, { label: "Modify", value: ProjectPermissionPkiSyncActions.Edit }, { label: "Remove", value: ProjectPermissionPkiSyncActions.Delete }, diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx index f50b39a92..5c858accc 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx @@ -21,6 +21,9 @@ import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { AddPoliciesButton } from "./AddPoliciesButton"; import { AppConnectionPermissionConditions } from "./AppConnectionPermissionConditions"; +import { CertificateAuthorityPermissionConditions } from "./CertificateAuthorityPermissionConditions"; +import { CertificatePermissionConditions } from "./CertificatePermissionConditions"; +import { CertificateProfilePermissionConditions } from "./CertificateProfilePermissionConditions"; import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions"; import { GeneralPermissionConditions } from "./GeneralPermissionConditions"; import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies"; @@ -96,6 +99,18 @@ export const renderConditionalComponents = ( return ; } + if (subject === ProjectPermissionSub.CertificateAuthorities) { + return ; + } + + if (subject === ProjectPermissionSub.Certificates) { + return ; + } + + if (subject === ProjectPermissionSub.CertificateProfiles) { + return ; + } + return ; } From 1458fddc152a749590c9d345da0a1df0ea2d5659 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Wed, 3 Dec 2025 00:26:05 -0300 Subject: [PATCH 2/9] Lint fix --- .../certificate-authority/certificate-authority-service.ts | 2 +- .../components/CertificateProfilesTab/ProfileRow.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 56e5c88d5..9a65ed821 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -723,7 +723,7 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionCertificateAuthorityActions.Read, ProjectPermissionSub.CertificateAuthorities ); diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx index 58818679f..5834feea3 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx @@ -105,8 +105,8 @@ export const ProfileRow = ({ : profile.certificateAuthority?.isExternal ? profile.certificateAuthority.name : caData?.configuration.friendlyName || - caData?.configuration.commonName || - profile.caId} + caData?.configuration.commonName || + profile.caId} From b53348a684934bae3fc10ff9469672cc7ed751b6 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Wed, 3 Dec 2025 11:05:27 -0300 Subject: [PATCH 3/9] Address greptile comments --- .../services/permission/project-permission.ts | 1 + .../certificate-authority-service.ts | 9 ++- .../src/services/pki-sync/pki-sync-service.ts | 62 ++++++++++++------- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 5a744e064..d9fb2ae6a 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -307,6 +307,7 @@ export type SecretSyncSubjectFields = { }; export type PkiSyncSubjectFields = { + subscriberName?: string; name: string; }; diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 9a65ed821..3c0b22bb8 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -332,8 +332,8 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateAuthorityActions.Read, - subject(ProjectPermissionSub.CertificateAuthorities, { name: "*" }) + ProjectPermissionCertificateAuthorityActions.List, + ProjectPermissionSub.CertificateAuthorities ); if (type === CaType.INTERNAL) { @@ -724,7 +724,10 @@ export const certificateAuthorityServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateAuthorityActions.Read, - ProjectPermissionSub.CertificateAuthorities + subject(ProjectPermissionSub.CertificateAuthorities, { + caId: ca.id, + name: ca.name + }) ); return ca; diff --git a/backend/src/services/pki-sync/pki-sync-service.ts b/backend/src/services/pki-sync/pki-sync-service.ts index 83d20079a..57a9a2ace 100644 --- a/backend/src/services/pki-sync/pki-sync-service.ts +++ b/backend/src/services/pki-sync/pki-sync-service.ts @@ -145,7 +145,10 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.Create, - subject(ProjectPermissionSub.PkiSyncs, { name: subscriber ? subscriber.name : name }) + subject(ProjectPermissionSub.PkiSyncs, { + subscriberName: subscriber?.name, + name + }) ); // Get the destination app type based on PKI sync destination @@ -233,9 +236,10 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.Edit, - currentSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { name: currentSubscriber.name }) - : subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) + subject(ProjectPermissionSub.PkiSyncs, { + subscriberName: currentSubscriber?.name, + name: pkiSync.name + }) ); if (name && name !== pkiSync.name) { @@ -329,9 +333,10 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.Delete, - pkiSyncSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { name: pkiSyncSubscriber.name }) - : subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) + subject(ProjectPermissionSub.PkiSyncs, { + subscriberName: pkiSyncSubscriber?.name, + name: pkiSync.name + }) ); return pkiSyncDAL.deleteById(id); @@ -404,9 +409,10 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.Read, - findSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { name: findSubscriber.name }) - : subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) + subject(ProjectPermissionSub.PkiSyncs, { + subscriberName: findSubscriber?.name, + name: pkiSync.name + }) ); const result = { @@ -440,9 +446,10 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.SyncCertificates, - syncSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { name: syncSubscriber.name }) - : subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) + subject(ProjectPermissionSub.PkiSyncs, { + subscriberName: syncSubscriber?.name, + name: pkiSync.name + }) ); await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: id }); @@ -481,9 +488,10 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.ImportCertificates, - importSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { name: importSubscriber.name }) - : subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) + subject(ProjectPermissionSub.PkiSyncs, { + subscriberName: importSubscriber?.name, + name: pkiSync.name + }) ); await pkiSyncQueue.queuePkiSyncImportCertificatesById({ syncId: id }); @@ -514,9 +522,10 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.RemoveCertificates, - removeSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { name: removeSubscriber.name }) - : subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) + subject(ProjectPermissionSub.PkiSyncs, { + subscriberName: removeSubscriber?.name, + name: pkiSync.name + }) ); await pkiSyncQueue.queuePkiSyncRemoveCertificatesById({ syncId: id }); @@ -554,9 +563,10 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.Edit, - pkiSyncSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { name: pkiSyncSubscriber.name }) - : subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) + subject(ProjectPermissionSub.PkiSyncs, { + subscriberName: pkiSyncSubscriber?.name, + name: pkiSync.name + }) ); await validateCertificatesProjectOwnership(certificateIds, pkiSync.projectId); @@ -598,7 +608,9 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.Edit, - subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) + subject(ProjectPermissionSub.PkiSyncs, { + name: pkiSync.name + }) ); const removedCount = await certificateSyncDAL.removeCertificates(pkiSyncId, certificateIds); @@ -639,7 +651,9 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.Read, - subject(ProjectPermissionSub.PkiSyncs, { name: pkiSync.name }) + subject(ProjectPermissionSub.PkiSyncs, { + name: pkiSync.name + }) ); const result = await certificateSyncDAL.findWithDetails({ From 022c2a860ed1a0839a3115ef2b6c2786797b806a Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Thu, 4 Dec 2025 04:58:32 -0300 Subject: [PATCH 4/9] Improve list endpoints permissions check and address PR comments --- .../ee/services/permission/default-roles.ts | 8 +- .../services/permission/project-permission.ts | 53 +-- .../src/lib/casl/permission-filter-utils.ts | 341 ++++++++++++++++++ .../src/lib/knex/permission-filter-utils.ts | 281 +++++++++++++++ .../acme/acme-certificate-authority-fns.ts | 20 +- .../azure-ad-cs-certificate-authority-fns.ts | 20 +- .../certificate-authority-dal.ts | 8 +- .../certificate-authority-service.ts | 25 +- .../certificate-profile-dal.ts | 21 +- .../certificate-profile-service.ts | 45 ++- .../certificate-template-v2-dal.ts | 19 + .../certificate-template-v2-service.ts | 18 +- .../certificate-v3/certificate-v3-service.ts | 10 +- .../services/certificate/certificate-dal.ts | 15 +- backend/src/services/pki-sync/pki-sync-dal.ts | 24 +- .../src/services/pki-sync/pki-sync-service.ts | 11 +- .../src/services/project/project-service.ts | 54 +-- docs/docs.json | 2 - .../context/ProjectPermissionContext/types.ts | 2 - .../CertAuthDetailsByIDPage.tsx | 2 - .../components/CaDetailsSection.tsx | 6 +- .../components/CaSection.tsx | 1 + .../components/CaTable.tsx | 5 - .../components/ExternalCaTable.tsx | 4 - .../PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx | 2 - .../components/PkiSyncActionTriggers.tsx | 3 +- .../components/PkiSyncCertificatesSection.tsx | 1 - .../components/PkiSyncDestinationSection.tsx | 3 +- .../components/PkiSyncDetailsSection.tsx | 4 +- .../PkiSyncFieldMappingsSection.tsx | 1 - .../PkiSyncOptionsSection.tsx | 1 - .../components/PkiSyncSourceSection.tsx | 1 - .../CertificatePermissionConditions.tsx | 1 - .../components/PermissionConditionHelpers.tsx | 1 + .../ProjectRoleModifySection.utils.tsx | 17 - 35 files changed, 860 insertions(+), 170 deletions(-) create mode 100644 backend/src/lib/casl/permission-filter-utils.ts create mode 100644 backend/src/lib/knex/permission-filter-utils.ts diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index a12f28709..4151face4 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -74,7 +74,6 @@ const buildAdminPermissionRules = () => { ProjectPermissionCertificateAuthorityActions.Create, ProjectPermissionCertificateAuthorityActions.Edit, ProjectPermissionCertificateAuthorityActions.Delete, - ProjectPermissionCertificateAuthorityActions.List, ProjectPermissionCertificateAuthorityActions.Renew, ProjectPermissionCertificateAuthorityActions.SignIntermediate ], @@ -111,7 +110,7 @@ const buildAdminPermissionRules = () => { ProjectPermissionCertificateActions.Delete, ProjectPermissionCertificateActions.ReadPrivateKey, ProjectPermissionCertificateActions.Import, - ProjectPermissionCertificateActions.List + ProjectPermissionCertificateActions.Read ], ProjectPermissionSub.Certificates ); @@ -119,7 +118,6 @@ const buildAdminPermissionRules = () => { can( [ ProjectPermissionCertificateProfileActions.Read, - ProjectPermissionCertificateProfileActions.List, ProjectPermissionCertificateProfileActions.Edit, ProjectPermissionCertificateProfileActions.Create, ProjectPermissionCertificateProfileActions.Delete, @@ -487,7 +485,7 @@ const buildMemberPermissionRules = () => { ProjectPermissionCertificateActions.Create, ProjectPermissionCertificateActions.Delete, ProjectPermissionCertificateActions.Import, - ProjectPermissionCertificateActions.List + ProjectPermissionCertificateActions.Read ], ProjectPermissionSub.Certificates ); @@ -495,7 +493,6 @@ const buildMemberPermissionRules = () => { can( [ ProjectPermissionCertificateProfileActions.Read, - ProjectPermissionCertificateProfileActions.List, ProjectPermissionCertificateProfileActions.Edit, ProjectPermissionCertificateProfileActions.Create, ProjectPermissionCertificateProfileActions.Delete @@ -621,7 +618,6 @@ const buildViewerPermissionRules = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); can(ProjectPermissionCertificateAuthorityActions.Read, ProjectPermissionSub.CertificateAuthorities); can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates); - can(ProjectPermissionCertificateActions.List, ProjectPermissionSub.Certificates); can(ProjectPermissionPkiTemplateActions.Read, ProjectPermissionSub.CertificateTemplates); can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index d9fb2ae6a..20f9c1f09 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -28,7 +28,6 @@ export enum ProjectPermissionCertificateAuthorityActions { Create = "create", Edit = "edit", Delete = "delete", - List = "list", Renew = "renew", SignIntermediate = "sign-intermediate" } @@ -38,7 +37,6 @@ export enum ProjectPermissionCertificateActions { Create = "create", Edit = "edit", Delete = "delete", - List = "list", ReadPrivateKey = "read-private-key", Import = "import" } @@ -129,7 +127,6 @@ export enum ProjectPermissionPkiSubscriberActions { export enum ProjectPermissionCertificateProfileActions { Read = "read", - List = "list", Create = "create", Edit = "edit", Delete = "delete", @@ -153,7 +150,6 @@ export enum ProjectPermissionPkiSyncActions { Create = "create", Edit = "edit", Delete = "delete", - List = "list", SyncCertificates = "sync-certificates", ImportCertificates = "import-certificates", RemoveCertificates = "remove-certificates" @@ -256,7 +252,6 @@ export enum ProjectPermissionSub { CertificateAuthorities = "certificate-authorities", Certificates = "certificates", CertificateTemplates = "certificate-templates", - CertificateTemplatesV2 = "certificate-templates-v2", SshCertificateAuthorities = "ssh-certificate-authorities", SshCertificates = "ssh-certificates", SshCertificateTemplates = "ssh-certificate-templates", @@ -453,13 +448,6 @@ export type ProjectPermissionSet = | (ForcedSubject & PkiTemplateSubjectFields) ) ] - | [ - ProjectPermissionActions, - ( - | ProjectPermissionSub.CertificateTemplatesV2 - | (ForcedSubject & CertificateTemplateV2SubjectFields) - ) - ] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] @@ -640,6 +628,17 @@ const PkiSyncConditionSchema = z [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] }) .partial() + ]), + subscriberName: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] + }) + .partial() ]) }) .partial(); @@ -756,6 +755,7 @@ const PkiTemplateConditionSchema = z z .object({ [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB], [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] }) @@ -875,7 +875,8 @@ const CertificateConditionSchema = z .object({ [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], - [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] }) .partial() ]) @@ -898,22 +899,6 @@ const CertificateProfileConditionSchema = z }) .partial(); -const CertificateTemplateV2ConditionSchema = z - .object({ - name: z.union([ - z.string(), - z - .object({ - [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], - [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], - [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], - [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] - }) - .partial() - ]) - }) - .partial(); - const GeneralPermissionSchema = [ z.object({ subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), @@ -1308,16 +1293,6 @@ 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.CertificateTemplatesV2).describe("The entity this permission pertains to."), - inverted: z.boolean().optional().describe("Whether rule allows or forbids."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( - "Describe what action an entity can take." - ), - conditions: CertificateTemplateV2ConditionSchema.describe( - "When specified, only matching conditions will be allowed to access given resource." - ).optional() - }), ...GeneralPermissionSchema ]); diff --git a/backend/src/lib/casl/permission-filter-utils.ts b/backend/src/lib/casl/permission-filter-utils.ts new file mode 100644 index 000000000..06dabe0a0 --- /dev/null +++ b/backend/src/lib/casl/permission-filter-utils.ts @@ -0,0 +1,341 @@ +import type { MongoAbility, MongoQuery, RawRuleOf } from "@casl/ability"; +import RE2 from "re2"; + +export interface PermissionFilterConfig { + operator: string; + value: unknown; + isPattern: boolean; + isInverted?: boolean; +} + +export type PermissionFilters = Record>; + +export interface ProcessedPermissionRules { + allowRules: Array>>; + forbidRules: Array>>; +} + +interface MongoRegexFilter { + $regex: RegExp; +} + +interface MongoEqFilter { + $eq: unknown; +} + +interface MongoInFilter { + $in: unknown[]; +} + +interface MongoNeFilter { + $ne: unknown; +} + +interface MongoGlobFilter { + $glob: unknown; +} + +/** + * Builds permission filters from CASL MongoDB-style conditions + * @param conditions - MongoDB-style conditions from CASL ability + * @param isInverted - Whether this rule is inverted (forbidden) + * @returns Record of field names to arrays of filter configurations + */ +export const buildPermissionFiltersFromConditions = (conditions: MongoQuery, isInverted = false): PermissionFilters => { + const permissionFilters: PermissionFilters = {}; + + function addFilterToField(key: string, operator: string, value: unknown, isPattern: boolean) { + if (!permissionFilters[key]) { + permissionFilters[key] = []; + } + + // Convert operators for inverted/forbidden rules + let finalOperator = operator; + if (isInverted) { + switch (operator) { + case "=": + finalOperator = "!="; + break; + case "!=": + finalOperator = "="; + break; + case "LIKE": + finalOperator = "NOT LIKE"; + break; + case "NOT LIKE": + finalOperator = "LIKE"; + break; + case "IN": + finalOperator = "NOT IN"; + break; + case "NOT IN": + finalOperator = "IN"; + break; + case ">": + finalOperator = "<="; + break; + case ">=": + finalOperator = "<"; + break; + case "<": + finalOperator = ">="; + break; + case "<=": + finalOperator = ">"; + break; + case "IS NULL": + finalOperator = "IS NOT NULL"; + break; + case "IS NOT NULL": + finalOperator = "IS NULL"; + break; + // Default: keep the same operator + default: + finalOperator = operator; + break; + } + } + + permissionFilters[key].push({ operator: finalOperator, value, isPattern, isInverted }); + } + + function processCondition(key: string, value: unknown) { + if (value && typeof value === "object") { + const valueObj = value as Record; + + const operatorKeys = ["$regex", "$eq", "$in", "$glob", "$ne"]; + const presentOperators = operatorKeys.filter((op) => op in valueObj); + + if (presentOperators.length > 1) { + if ("$eq" in valueObj) { + addFilterToField(key, "=", valueObj.$eq, false); + } + if ("$glob" in valueObj) { + addFilterToField(key, "LIKE", valueObj.$glob, true); + } + if ("$regex" in valueObj) { + const regexValue = valueObj.$regex as RegExp; + const regexPattern = regexValue.source; + const globPattern = regexPattern + .replace(new RE2("^\\\\\\^"), "") + .replace(new RE2("\\\\\\$$"), "") + .replace(new RE2("\\\\\\.\\*", "g"), "*"); + addFilterToField(key, "LIKE", globPattern, true); + } + if ("$ne" in valueObj) { + const valueStr = String(valueObj.$ne); + const hasWildcards = valueStr.includes("*") || valueStr.includes("?"); + addFilterToField(key, hasWildcards ? "NOT LIKE" : "!=", valueObj.$ne, hasWildcards); + } + if ("$in" in valueObj) { + const inValues = valueObj.$in as unknown[]; + addFilterToField(key, "IN", inValues, false); + } + } else if ("$regex" in value) { + const regexFilter = value as MongoRegexFilter; + const regexPattern = regexFilter.$regex.source; + const globPattern = regexPattern + .replace(new RE2("^\\\\\\^"), "") + .replace(new RE2("\\\\\\$$"), "") + .replace(new RE2("\\\\\\.\\*", "g"), "*"); + addFilterToField(key, "LIKE", globPattern, true); + } else if ("$eq" in value) { + const eqFilter = value as MongoEqFilter; + addFilterToField(key, "=", eqFilter.$eq, false); + } else if ("$in" in value) { + const inFilter = value as MongoInFilter; + addFilterToField(key, "IN", inFilter.$in, false); + } else if ("$glob" in value) { + const globFilter = value as MongoGlobFilter; + addFilterToField(key, "LIKE", globFilter.$glob, true); + } else if ("$ne" in value) { + const neFilter = value as MongoNeFilter; + const valueStr = String(neFilter.$ne); + const hasWildcards = valueStr.includes("*") || valueStr.includes("?"); + addFilterToField(key, hasWildcards ? "NOT LIKE" : "!=", neFilter.$ne, hasWildcards); + } + } else { + addFilterToField(key, "=", value, false); + } + } + + function processConditions(mongoConditions: MongoQuery) { + if ( + mongoConditions && + typeof mongoConditions === "object" && + "$or" in mongoConditions && + Array.isArray(mongoConditions.$or) + ) { + mongoConditions.$or.forEach((orCondition: MongoQuery) => { + processConditions(orCondition); + }); + } else if (mongoConditions && typeof mongoConditions === "object") { + Object.entries(mongoConditions).forEach(([key, value]) => { + if (key.startsWith("$")) return; + processCondition(key, value); + }); + } + } + + if (conditions && typeof conditions === "object") { + processConditions(conditions); + } + + return permissionFilters; +}; + +/** + * Convert CASL condition object to MongoDB-compatible database filter + * @param condition - CASL condition object + * @returns MongoDB filter object + */ +export function convertConditionToDbFilter(condition: Record): Record { + const dbFilter: Record = {}; + + for (const key in condition) { + if (Object.prototype.hasOwnProperty.call(condition, key)) { + const value = condition[key]; + + if (value && typeof value === "object" && !Array.isArray(value)) { + const operator = Object.keys(value)[0]; + const opValue = (value as Record)[operator]; + + switch (operator) { + case "$glob": { + // Convert "*.es" glob → regex /^.*\.es$/i + const regex = new RegExp(`^${String(opValue).replace(/\*/g, ".*")}$`, "i"); + dbFilter[key] = { $regex: regex }; + break; + } + + case "$in": + dbFilter[key] = { $in: opValue }; + break; + + case "$eq": + dbFilter[key] = { $eq: opValue }; + break; + + default: + dbFilter[key] = value; + break; + } + } else { + dbFilter[key] = { $eq: value }; + } + } + } + + return dbFilter; +} + +/** + * Extract ALL conditions for a subject and action, + * converting them into DB-safe filter objects. + * @param ability - CASL MongoAbility instance + * @param action - Permission action to filter for + * @param subjectName - Permission subject to filter for + * @returns MongoDB filter object with $or conditions + */ +export function getDbFiltersForAbility( + ability: MongoAbility, + action: string, + subjectName: string +): Record { + const matchingRules = ability.rules.filter((rule: RawRuleOf) => { + const actionMatches = Array.isArray(rule.action) ? rule.action.includes(action) : rule.action === action; + const subjectMatches = Array.isArray(rule.subject) + ? rule.subject.includes(subjectName) + : rule.subject === subjectName; + return actionMatches && subjectMatches && rule.conditions; + }); + + const converted = matchingRules.map((rule: RawRuleOf) => + convertConditionToDbFilter(rule.conditions as Record) + ); + + if (converted.length === 0) { + return {}; + } + + return { $or: converted }; +} + +/** + * Extract permission filters for a subject and action, + * converting them into ProcessedPermissionRules format for use with Knex queries. + * @param ability - CASL MongoAbility instance + * @param action - Permission action to filter for + * @param subjectName - Permission subject to filter for + * @returns ProcessedPermissionRules object for use with applyPermissionFiltersToQuery + */ +export function getProcessedPermissionRules( + ability: MongoAbility, + action: string, + subjectName: string +): ProcessedPermissionRules { + const matchingRules = ability.rules.filter((rule: RawRuleOf) => { + const actionMatches = Array.isArray(rule.action) ? rule.action.includes(action) : rule.action === action; + const subjectMatches = Array.isArray(rule.subject) + ? rule.subject.includes(subjectName) + : rule.subject === subjectName; + return actionMatches && subjectMatches && rule.conditions; + }); + + const allowRules: Array>> = []; + const forbidRules: Array>> = []; + + matchingRules.forEach((rule: RawRuleOf) => { + if (rule.conditions) { + const isInverted = rule.inverted || false; + const ruleFilters = buildPermissionFiltersFromConditions(rule.conditions, isInverted); + + if (isInverted) { + forbidRules.push(ruleFilters); + } else { + allowRules.push(ruleFilters); + } + } + }); + + return { allowRules, forbidRules }; +} + +/** + * Extract permission filters for a subject and action, + * converting them into PermissionFilters format for use with Knex queries. + * This is a simplified version that maintains backward compatibility. + * @param ability - CASL MongoAbility instance + * @param action - Permission action to filter for + * @param subjectName - Permission subject to filter for + * @returns PermissionFilters object for use with applyPermissionFiltersToQuery + */ +export function getPermissionFiltersForAbility( + ability: MongoAbility, + action: string, + subjectName: string +): PermissionFilters { + const processedRules = getProcessedPermissionRules(ability, action, subjectName); + + const mergedFilters: PermissionFilters = {}; + + processedRules.allowRules.forEach((rule) => { + Object.entries(rule).forEach(([key, filterConfigs]) => { + if (!mergedFilters[key]) { + mergedFilters[key] = []; + } + mergedFilters[key].push(...filterConfigs); + }); + }); + + processedRules.forbidRules.forEach((rule) => { + Object.entries(rule).forEach(([key, filterConfigs]) => { + if (!mergedFilters[key]) { + mergedFilters[key] = []; + } + mergedFilters[key].push(...filterConfigs); + }); + }); + + return mergedFilters; +} diff --git a/backend/src/lib/knex/permission-filter-utils.ts b/backend/src/lib/knex/permission-filter-utils.ts new file mode 100644 index 000000000..3b8f448ec --- /dev/null +++ b/backend/src/lib/knex/permission-filter-utils.ts @@ -0,0 +1,281 @@ +import type { Knex } from "knex"; +import RE2 from "re2"; + +export interface PermissionFilterConfig { + operator: string; + value: unknown; + isPattern: boolean; + isInverted?: boolean; +} + +export type PermissionFilters = Record>; + +export interface ProcessedPermissionRules { + allowRules: Array>>; + forbidRules: Array>>; +} + +/** + * Applies permission filters to a Knex query for any table + * @param query - The Knex query builder instance + * @param tableName - The name of the table to apply filters to + * @param permissionFilters - Record of field names to arrays of filter configurations + * @returns The modified query builder with permission filters applied + */ +export const applyPermissionFiltersToQuery = ( + originalQuery: Knex.QueryBuilder, + tableName: string, + permissionFilters?: PermissionFilters +): Knex.QueryBuilder => { + if (!permissionFilters) { + return originalQuery; + } + + let query = originalQuery; + + Object.entries(permissionFilters).forEach(([key, filterConfigs]) => { + filterConfigs.forEach((filterConfig) => { + if (filterConfig.value !== undefined && filterConfig.value !== null) { + const { operator, value, isPattern } = filterConfig; + const fieldName = `${tableName}.${key}`; + + switch (operator) { + case "=": + query = query.andWhere(fieldName, "=", value as string | number); + break; + case "!=": + query = query.andWhere(fieldName, "!=", value as string | number); + break; + case "LIKE": { + const likePattern = isPattern ? String(value).replace(new RE2("\\*", "g"), "%") : String(value); + query = query.andWhere(fieldName, "like", likePattern); + break; + } + case "NOT LIKE": { + const notLikePattern = isPattern ? String(value).replace(new RE2("\\*", "g"), "%") : String(value); + query = query.andWhere(fieldName, "not like", notLikePattern); + break; + } + case "IN": { + const inValues = Array.isArray(value) ? value : [value]; + query = query.andWhere(fieldName, "in", inValues as (string | number)[]); + break; + } + case "NOT IN": { + const notInValues = Array.isArray(value) ? value : [value]; + query = query.andWhere(fieldName, "not in", notInValues as (string | number)[]); + break; + } + case ">": + query = query.andWhere(fieldName, ">", value as string | number); + break; + case ">=": + query = query.andWhere(fieldName, ">=", value as string | number); + break; + case "<": + query = query.andWhere(fieldName, "<", value as string | number); + break; + case "<=": + query = query.andWhere(fieldName, "<=", value as string | number); + break; + case "IS NULL": + query = query.andWhere(fieldName, "is", null); + break; + case "IS NOT NULL": + query = query.andWhere(fieldName, "is not", null); + break; + default: + query = query.andWhere(fieldName, "=", value as string | number); + break; + } + } + }); + }); + + return query; +}; + +/** + * Applies a single filter configuration to a query + * @param query - The Knex query builder instance + * @param tableName - The name of the table to apply filters to + * @param key - The field name + * @param filterConfig - The filter configuration + */ +const applySingleFilter = ( + query: Knex.QueryBuilder, + tableName: string, + key: string, + filterConfig: PermissionFilterConfig +): void => { + if (filterConfig.value !== undefined && filterConfig.value !== null) { + const { operator, value, isPattern } = filterConfig; + const fieldName = `${tableName}.${key}`; + + switch (operator) { + case "=": + void query.andWhere(fieldName, "=", value as string | number); + break; + case "!=": + void query.andWhere(fieldName, "!=", value as string | number); + break; + case "LIKE": { + const likePattern = isPattern ? String(value).replace(new RE2("\\*", "g"), "%") : String(value); + void query.andWhere(fieldName, "like", likePattern); + break; + } + case "NOT LIKE": { + const notLikePattern = isPattern ? String(value).replace(new RE2("\\*", "g"), "%") : String(value); + void query.andWhere(fieldName, "not like", notLikePattern); + break; + } + case "IN": { + const inValues = Array.isArray(value) ? value : [value]; + void query.andWhere(fieldName, "in", inValues as (string | number)[]); + break; + } + case "NOT IN": { + const notInValues = Array.isArray(value) ? value : [value]; + void query.andWhere(fieldName, "not in", notInValues as (string | number)[]); + break; + } + case ">": + void query.andWhere(fieldName, ">", value as string | number); + break; + case ">=": + void query.andWhere(fieldName, ">=", value as string | number); + break; + case "<": + void query.andWhere(fieldName, "<", value as string | number); + break; + case "<=": + void query.andWhere(fieldName, "<=", value as string | number); + break; + case "IS NULL": + void query.andWhere(fieldName, "is", null); + break; + case "IS NOT NULL": + void query.andWhere(fieldName, "is not", null); + break; + default: + void query.andWhere(fieldName, "=", value as string | number); + break; + } + } +}; + +/** + * Applies complex permission rules to a Knex query with proper OR/AND logic + * @param query - The Knex query builder instance + * @param tableName - The name of the table to apply filters to + * @param processedRules - Processed permission rules with allow and forbid rules + * @returns The modified query builder with permission rules applied + */ +export const applyProcessedPermissionRulesToQuery = ( + originalQuery: Knex.QueryBuilder, + tableName: string, + processedRules?: ProcessedPermissionRules +): Knex.QueryBuilder => { + if (!processedRules || (processedRules.allowRules.length === 0 && processedRules.forbidRules.length === 0)) { + return originalQuery; + } + + let query = originalQuery; + + if (processedRules.allowRules.length > 0) { + query = query.andWhere((allowBuilder) => { + processedRules.allowRules.forEach((rule, index) => { + const ruleBuilder = (ruleSubBuilder: Knex.QueryBuilder) => { + Object.entries(rule).forEach(([key, filterConfigs]) => { + filterConfigs.forEach((filterConfig) => { + applySingleFilter(ruleSubBuilder, tableName, key, filterConfig); + }); + }); + }; + + if (index === 0) { + void allowBuilder.where(ruleBuilder); + } else { + void allowBuilder.orWhere(ruleBuilder); + } + }); + }); + } + + if (processedRules.forbidRules.length > 0) { + processedRules.forbidRules.forEach((forbidRule) => { + query = query.andWhere((forbidBuilder) => { + let hasConditions = false; + Object.entries(forbidRule).forEach(([key, filterConfigs]) => { + filterConfigs.forEach((filterConfig) => { + const negatedConfig = { ...filterConfig }; + + switch (filterConfig.operator) { + case "=": + negatedConfig.operator = "!="; + break; + case "!=": + negatedConfig.operator = "="; + break; + case "LIKE": + negatedConfig.operator = "NOT LIKE"; + break; + case "NOT LIKE": + negatedConfig.operator = "LIKE"; + break; + case "IN": + negatedConfig.operator = "NOT IN"; + break; + case "NOT IN": + negatedConfig.operator = "IN"; + break; + case ">": + negatedConfig.operator = "<="; + break; + case ">=": + negatedConfig.operator = "<"; + break; + case "<": + negatedConfig.operator = ">="; + break; + case "<=": + negatedConfig.operator = ">"; + break; + case "IS NULL": + negatedConfig.operator = "IS NOT NULL"; + break; + case "IS NOT NULL": + negatedConfig.operator = "IS NULL"; + break; + default: + negatedConfig.operator = "!="; + break; + } + + if (hasConditions) { + void forbidBuilder.orWhere((subBuilder) => { + applySingleFilter(subBuilder, tableName, key, negatedConfig); + }); + } else { + void forbidBuilder.where((subBuilder) => { + applySingleFilter(subBuilder, tableName, key, negatedConfig); + }); + hasConditions = true; + } + }); + }); + }); + }); + } + + return query; +}; + +/** + * Sanitizes a string value for safe use in SQL LIKE queries + * @param value - The string value to sanitize + * @returns The sanitized string with SQL special characters escaped + */ +export const sanitizeForLike = (value: string): string => { + return String(value).replace(new RE2("[%_\\\\]", "g"), "\\$&"); +}; diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts index b48b1076f..6ef479971 100644 --- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts @@ -715,11 +715,21 @@ export const AcmeCertificateAuthorityFns = ({ return castDbEntryToAcmeCertificateAuthority(updatedCa); }; - const listCertificateAuthorities = async ({ projectId }: { projectId: string }) => { - const cas = await certificateAuthorityDAL.findWithAssociatedCa({ - [`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, - [`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.ACME - }); + const listCertificateAuthorities = async ({ + projectId, + permissionFilters + }: { + projectId: string; + permissionFilters?: import("@app/lib/knex/permission-filter-utils").PermissionFilters; + }) => { + const cas = await certificateAuthorityDAL.findWithAssociatedCa( + { + [`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, + [`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.ACME + }, + {}, + permissionFilters + ); return cas.map(castDbEntryToAcmeCertificateAuthority); }; diff --git a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts index 421da28aa..eea877b6e 100644 --- a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts @@ -798,11 +798,21 @@ export const AzureAdCsCertificateAuthorityFns = ({ return castDbEntryToAzureAdCsCertificateAuthority(updatedCa); }; - const listCertificateAuthorities = async ({ projectId }: { projectId: string }) => { - const cas = await certificateAuthorityDAL.findWithAssociatedCa({ - [`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, - [`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.AZURE_AD_CS - }); + const listCertificateAuthorities = async ({ + projectId, + permissionFilters + }: { + projectId: string; + permissionFilters?: import("@app/lib/knex/permission-filter-utils").PermissionFilters; + }) => { + const cas = await certificateAuthorityDAL.findWithAssociatedCa( + { + [`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, + [`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.AZURE_AD_CS + }, + {}, + permissionFilters + ); return cas.map(castDbEntryToAzureAdCsCertificateAuthority); }; diff --git a/backend/src/services/certificate-authority/certificate-authority-dal.ts b/backend/src/services/certificate-authority/certificate-authority-dal.ts index 352675441..5809b04d9 100644 --- a/backend/src/services/certificate-authority/certificate-authority-dal.ts +++ b/backend/src/services/certificate-authority/certificate-authority-dal.ts @@ -4,6 +4,7 @@ import { TDbClient } from "@app/db"; import { CertificateAuthoritiesSchema, TableName, TCertificateAuthorities } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { buildFindFilter, ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex"; +import { applyPermissionFiltersToQuery, type PermissionFilters } from "@app/lib/knex/permission-filter-utils"; export type TCertificateAuthorityDALFactory = ReturnType; @@ -220,10 +221,11 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { const findWithAssociatedCa = async ( filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string; serialNumber?: string }, { offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt = {}, + permissionFilters?: PermissionFilters, tx?: Knex ) => { try { - const query = (tx || db.replicaNode())(TableName.CertificateAuthority) + let query = (tx || db.replicaNode())(TableName.CertificateAuthority) .leftJoin( TableName.InternalCertificateAuthority, `${TableName.CertificateAuthority}.id`, @@ -268,6 +270,10 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { db.ref("appConnectionId").withSchema(TableName.ExternalCertificateAuthority).as("externalAppConnectionId") ); + if (permissionFilters) { + query = applyPermissionFiltersToQuery(query, TableName.CertificateAuthority, permissionFilters) as typeof query; + } + if (limit) void query.limit(limit); if (offset) void query.offset(offset); if (sort) { diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 3c0b22bb8..4be0de84c 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -6,6 +6,7 @@ import { ProjectPermissionCertificateAuthorityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { getPermissionFiltersForAbility } from "@app/lib/casl/permission-filter-utils"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; @@ -332,15 +333,25 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateAuthorityActions.List, + ProjectPermissionCertificateAuthorityActions.Read, + ProjectPermissionSub.CertificateAuthorities + ); + + const permissionFilters = getPermissionFiltersForAbility( + permission, + ProjectPermissionCertificateAuthorityActions.Read, ProjectPermissionSub.CertificateAuthorities ); if (type === CaType.INTERNAL) { - const cas = await certificateAuthorityDAL.findWithAssociatedCa({ - [`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, - $notNull: [`${TableName.InternalCertificateAuthority}.id` as "id"] - }); + const cas = await certificateAuthorityDAL.findWithAssociatedCa( + { + [`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, + $notNull: [`${TableName.InternalCertificateAuthority}.id` as "id"] + }, + {}, + permissionFilters + ); return cas .filter((ca): ca is typeof ca & { internalCa: NonNullable } => Boolean(ca.internalCa)) @@ -356,11 +367,11 @@ export const certificateAuthorityServiceFactory = ({ } if (type === CaType.ACME) { - return acmeFns.listCertificateAuthorities({ projectId }); + return acmeFns.listCertificateAuthorities({ projectId, permissionFilters }); } if (type === CaType.AZURE_AD_CS) { - return azureAdCsFns.listCertificateAuthorities({ projectId }); + return azureAdCsFns.listCertificateAuthorities({ projectId, permissionFilters }); } throw new BadRequestError({ message: "Invalid certificate authority type" }); diff --git a/backend/src/services/certificate-profile/certificate-profile-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index 7a6494d34..5bab780eb 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -4,6 +4,7 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { applyPermissionFiltersToQuery, type PermissionFilters } from "@app/lib/knex/permission-filter-utils"; import { EnrollmentType, @@ -276,6 +277,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { issuerType?: IssuerType; caId?: string; } = {}, + permissionFilters?: PermissionFilters, tx?: Knex ): Promise => { try { @@ -308,7 +310,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.issuerType`, issuerType); } - const query = baseQuery + let query = baseQuery .leftJoin( TableName.CertificateAuthority, `${TableName.PkiCertificateProfile}.caId`, @@ -354,6 +356,14 @@ export const certificateProfileDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeId") ); + if (permissionFilters) { + query = applyPermissionFiltersToQuery( + query, + TableName.PkiCertificateProfile, + permissionFilters + ) as typeof query; + } + const results = (await query .orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc") .offset(offset) @@ -432,6 +442,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { issuerType?: IssuerType; caId?: string; } = {}, + permissionFilters?: PermissionFilters, tx?: Knex ): Promise => { try { @@ -459,6 +470,14 @@ export const certificateProfileDALFactory = (db: TDbClient) => { query = query.where({ issuerType }); } + if (permissionFilters) { + query = applyPermissionFiltersToQuery( + query, + TableName.PkiCertificateProfile, + permissionFilters + ) as typeof query; + } + const result = await query.count("*").first(); return parseInt((result as unknown as { count: string }).count || "0", 10); } catch (error) { diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index aaa29a772..20da7891a 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -10,6 +10,7 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { buildUrl } from "@app/ee/services/pki-acme/pki-acme-fns"; +import { getPermissionFiltersForAbility } from "@app/lib/casl/permission-filter-utils"; import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; @@ -745,25 +746,39 @@ export const certificateProfileServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateProfileActions.List, + ProjectPermissionCertificateProfileActions.Read, ProjectPermissionSub.CertificateProfiles ); - const profiles = await certificateProfileDAL.findByProjectId(projectId, { - offset, - limit, - search, - enrollmentType, - issuerType, - caId - }); + const permissionFilters = getPermissionFiltersForAbility( + permission, + ProjectPermissionCertificateProfileActions.Read, + ProjectPermissionSub.CertificateProfiles + ); - const totalCount = await certificateProfileDAL.countByProjectId(projectId, { - search, - enrollmentType, - issuerType, - caId - }); + const profiles = await certificateProfileDAL.findByProjectId( + projectId, + { + offset, + limit, + search, + enrollmentType, + issuerType, + caId + }, + permissionFilters + ); + + const totalCount = await certificateProfileDAL.countByProjectId( + projectId, + { + search, + enrollmentType, + issuerType, + caId + }, + permissionFilters + ); const convertedProfiles = await Promise.all( profiles.map(async (profile) => { diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts index 3b935f26a..a2ed17a4c 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts @@ -5,6 +5,7 @@ import { TableName } from "@app/db/schemas"; import { TPkiCertificateTemplatesV2Insert } from "@app/db/schemas/pki-certificate-templates-v2"; import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; +import { applyPermissionFiltersToQuery, type PermissionFilters } from "@app/lib/knex/permission-filter-utils"; import { TCertificateTemplateV2, @@ -133,6 +134,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { limit?: number; search?: string; } = {}, + permissionFilters?: PermissionFilters, tx?: Knex ) => { try { @@ -146,6 +148,14 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { }); } + if (permissionFilters) { + query = applyPermissionFiltersToQuery( + query, + TableName.PkiCertificateTemplateV2, + permissionFilters + ) as typeof query; + } + const certificateTemplatesV2 = await query.orderBy("createdAt", "desc").offset(offset).limit(limit); return certificateTemplatesV2.map((template: Record) => parseJsonFields(template)); @@ -159,6 +169,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { options: { search?: string; } = {}, + permissionFilters?: PermissionFilters, tx?: Knex ) => { try { @@ -172,6 +183,14 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { }); } + if (permissionFilters) { + query = applyPermissionFiltersToQuery( + query, + TableName.PkiCertificateTemplateV2, + permissionFilters + ) as typeof query; + } + const result = await query.count("*").first(); return parseInt((result as unknown as { count: string }).count || "0", 10); } catch (error) { diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts index 9cc3d22c4..36d77e4ff 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts @@ -8,6 +8,7 @@ import { ProjectPermissionPkiTemplateActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { getPermissionFiltersForAbility } from "@app/lib/casl/permission-filter-utils"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -871,13 +872,18 @@ export const certificateTemplateV2ServiceFactory = ({ ProjectPermissionSub.CertificateTemplates ); - const templates = await certificateTemplateV2DAL.findByProjectId(projectId, { - offset, - limit, - search - }); + const permissionFilters = getPermissionFiltersForAbility( + permission, + ProjectPermissionPkiTemplateActions.Read, + ProjectPermissionSub.CertificateTemplates + ); + const templates = await certificateTemplateV2DAL.findByProjectId( + projectId, + { offset, limit, search }, + permissionFilters + ); - const totalCount = await certificateTemplateV2DAL.countByProjectId(projectId, { search }); + const totalCount = await certificateTemplateV2DAL.countByProjectId(projectId, { search }, permissionFilters); return { templates, diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index e50df78bc..af6162d70 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -1491,10 +1491,12 @@ export const certificateV3ServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateProfileActions.IssueCert, - subject(ProjectPermissionSub.CertificateProfiles, { slug: profile?.slug || "*" }) - ); + if (profile) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateProfileActions.IssueCert, + subject(ProjectPermissionSub.CertificateProfiles, { slug: profile.slug }) + ); + } } const issuerType = profile?.issuerType || (originalCert.caId ? IssuerType.CA : IssuerType.SELF_SIGNED); diff --git a/backend/src/services/certificate/certificate-dal.ts b/backend/src/services/certificate/certificate-dal.ts index 7af79319b..f61c46c28 100644 --- a/backend/src/services/certificate/certificate-dal.ts +++ b/backend/src/services/certificate/certificate-dal.ts @@ -4,6 +4,7 @@ import { TDbClient } from "@app/db"; import { TableName, TCertificates } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { applyPermissionFiltersToQuery, type PermissionFilters } from "@app/lib/knex/permission-filter-utils"; import { CertStatus } from "./certificate-types"; @@ -140,7 +141,8 @@ export const certificateDALFactory = (db: TDbClient) => { const findActiveCertificatesForSync = async ( filter: Partial, - options?: { limit?: number; offset?: number } + options?: { limit?: number; offset?: number }, + permissionFilters?: PermissionFilters ): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => { try { let query = db @@ -163,6 +165,10 @@ export const certificateDALFactory = (db: TDbClient) => { } }); + if (permissionFilters) { + query = applyPermissionFiltersToQuery(query, TableName.Certificate, permissionFilters) as typeof query; + } + if (options?.offset) { query = query.offset(options.offset); } @@ -267,7 +273,8 @@ export const certificateDALFactory = (db: TDbClient) => { const findWithPrivateKeyInfo = async ( filter: Partial, - options?: { offset?: number; limit?: number; sort?: [string, "asc" | "desc"][] } + options?: { offset?: number; limit?: number; sort?: [string, "asc" | "desc"][] }, + permissionFilters?: PermissionFilters ): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => { try { let query = db @@ -287,6 +294,10 @@ export const certificateDALFactory = (db: TDbClient) => { } }); + if (permissionFilters) { + query = applyPermissionFiltersToQuery(query, TableName.Certificate, permissionFilters) as typeof query; + } + if (options?.offset) { query = query.offset(options.offset); } diff --git a/backend/src/services/pki-sync/pki-sync-dal.ts b/backend/src/services/pki-sync/pki-sync-dal.ts index 460bbfc42..294ebff80 100644 --- a/backend/src/services/pki-sync/pki-sync-dal.ts +++ b/backend/src/services/pki-sync/pki-sync-dal.ts @@ -4,6 +4,7 @@ import { TDbClient } from "@app/db"; import { TableName, TPkiSyncs } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex"; +import { applyPermissionFiltersToQuery, type PermissionFilters } from "@app/lib/knex/permission-filter-utils"; import { PkiSync } from "./pki-sync-enums"; @@ -45,13 +46,15 @@ const basePkiSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: PkiSyncF const basePkiSyncWithSubscriberQuery = ({ filter, db, - tx + tx, + permissionFilters }: { db: TDbClient; filter?: PkiSyncFindFilter; tx?: Knex; + permissionFilters?: PermissionFilters; }) => { - const query = (tx || db.replicaNode())(TableName.PkiSync) + let query = (tx || db.replicaNode())(TableName.PkiSync) .leftJoin(TableName.AppConnection, `${TableName.PkiSync}.connectionId`, `${TableName.AppConnection}.id`) .leftJoin(TableName.PkiSubscriber, `${TableName.PkiSync}.subscriberId`, `${TableName.PkiSubscriber}.id`) .select(selectAllTableCols(TableName.PkiSync)) @@ -82,6 +85,10 @@ const basePkiSyncWithSubscriberQuery = ({ void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.PkiSync, filter))); } + if (permissionFilters) { + query = applyPermissionFiltersToQuery(query, TableName.PkiSync, permissionFilters) as typeof query; + } + return query; }; @@ -184,9 +191,18 @@ export const pkiSyncDALFactory = (db: TDbClient) => { } }; - const findByProjectIdWithSubscribers = async (projectId: string, tx?: Knex) => { + const findByProjectIdWithSubscribers = async ( + projectId: string, + permissionFilters?: PermissionFilters, + tx?: Knex + ) => { try { - const pkiSyncs = await basePkiSyncWithSubscriberQuery({ filter: { projectId }, db, tx }); + const pkiSyncs = await basePkiSyncWithSubscriberQuery({ + filter: { projectId }, + db, + tx, + permissionFilters + }); return pkiSyncs.map(expandPkiSyncWithSubscriber); } catch (error) { throw new DatabaseError({ error, name: "Find By Project ID With Subscribers - PKI Sync" }); diff --git a/backend/src/services/pki-sync/pki-sync-service.ts b/backend/src/services/pki-sync/pki-sync-service.ts index 57a9a2ace..2a4044208 100644 --- a/backend/src/services/pki-sync/pki-sync-service.ts +++ b/backend/src/services/pki-sync/pki-sync-service.ts @@ -4,6 +4,7 @@ import { ActionProjectType, TCertificateSyncs } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionPkiSyncActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { getPermissionFiltersForAbility } from "@app/lib/casl/permission-filter-utils"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; @@ -355,9 +356,15 @@ export const pkiSyncServiceFactory = ({ projectId }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.List, ProjectPermissionSub.PkiSyncs); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs); - const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId); + const permissionFilters = getPermissionFiltersForAbility( + permission, + ProjectPermissionPkiSyncActions.Read, + ProjectPermissionSub.PkiSyncs + ); + + const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId, permissionFilters); if (certificateId) { const syncsWithCertificateInfo = await Promise.all( diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 9c24171bb..2ea6503eb 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -40,6 +40,7 @@ import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certific import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal"; import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal"; import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; +import { buildPermissionFiltersFromConditions, getDbFiltersForAbility } from "@app/lib/casl/permission-filter-utils"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { DatabaseErrorCode } from "@app/lib/error-codes"; @@ -912,7 +913,7 @@ export const projectServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateAuthorityActions.List, + ProjectPermissionCertificateAuthorityActions.Read, ProjectPermissionSub.CertificateAuthorities ); @@ -960,39 +961,44 @@ export const projectServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateActions.List, + ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates ); + const filters = getDbFiltersForAbility( + permission, + ProjectPermissionCertificateActions.Read, + ProjectPermissionSub.Certificates + ); + + const regularFilters = { + projectId, + ...(friendlyName && { friendlyName }), + ...(commonName && { commonName }) + }; + const permissionFilters = buildPermissionFiltersFromConditions(filters || {}); + const certificates = forPkiSync - ? await certificateDAL.findActiveCertificatesForSync( - { - projectId, - ...(friendlyName && { friendlyName }), - ...(commonName && { commonName }) - }, - { offset, limit } - ) + ? await certificateDAL.findActiveCertificatesForSync(regularFilters, { offset, limit }, permissionFilters) : await certificateDAL.findWithPrivateKeyInfo( + regularFilters, { - projectId, - ...(friendlyName && { friendlyName }), - ...(commonName && { commonName }) + offset, + limit, + sort: [["notAfter", "desc"]] }, - { offset, limit, sort: [["notAfter", "desc"]] } + permissionFilters ); + const countFilter = { + projectId, + ...(regularFilters.friendlyName && { friendlyName: String(regularFilters.friendlyName) }), + ...(regularFilters.commonName && { commonName: String(regularFilters.commonName) }) + }; + const count = forPkiSync - ? await certificateDAL.countActiveCertificatesForSync({ - projectId, - friendlyName, - commonName - }) - : await certificateDAL.countCertificatesInProject({ - projectId, - friendlyName, - commonName - }); + ? await certificateDAL.countActiveCertificatesForSync(countFilter) + : await certificateDAL.countCertificatesInProject(countFilter); return { certificates, diff --git a/docs/docs.json b/docs/docs.json index be76a10c1..5a3d965fd 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -2493,8 +2493,6 @@ "pages": [ "api-reference/endpoints/certificates/list", "api-reference/endpoints/certificates/read", - "api-reference/endpoints/certificates/issue-certificate", - "api-reference/endpoints/certificates/sign-certificate", "api-reference/endpoints/certificates/renew", "api-reference/endpoints/certificates/update-config", "api-reference/endpoints/certificates/revoke", diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index d8406ab25..83eb2fecd 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -391,8 +391,6 @@ export type CertificateSubjectFields = { commonName?: string; altNames?: string; serialNumber?: string; - friendlyName?: string; - status?: string; }; export type CertificateProfileSubjectFields = { diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx index 22a3ab1d6..4f12c3950 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx @@ -93,7 +93,6 @@ const Page = () => { @@ -128,7 +127,6 @@ const Page = () => { diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaDetailsSection.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaDetailsSection.tsx index 489c59984..8f88a3f2e 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaDetailsSection.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaDetailsSection.tsx @@ -42,7 +42,7 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {

CA Details

{(isAllowed) => { return ( @@ -159,7 +159,7 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => { {ca.status === CaStatus.ACTIVE && ( {(isAllowed) => { return ( @@ -195,7 +195,7 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => { {ca.status === CaStatus.PENDING_CERTIFICATE && ( {(isAllowed) => { return ( diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx index 145248bae..2870f390d 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx @@ -104,6 +104,7 @@ export const CaSection = () => { : "This action will prevent the CA from issuing new certificates." } onChange={(isOpen) => handlePopUpToggle("caStatus", isOpen)} + buttonText="Confirm" deleteKey="confirm" onDeleteApproved={() => onUpdateCaStatus(popUp?.caStatus?.data as { caId: string; status: CaStatus }) diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx index 06cb97532..b52ca3daa 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx @@ -81,7 +81,6 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { const canReadCa = permission.can( ProjectPermissionCertificateAuthorityActions.Read, subject(ProjectPermissionSub.CertificateAuthorities, { - caId: ca.id, name: ca.name }) ); @@ -136,7 +135,6 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { @@ -164,7 +162,6 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { @@ -192,7 +189,6 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { @@ -223,7 +219,6 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx index 6d2b56f3f..e31320d59 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx @@ -75,7 +75,6 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => { const canReadCa = permission.can( ProjectPermissionCertificateAuthorityActions.Read, subject(ProjectPermissionSub.CertificateAuthorities, { - caId: ca.id, name: ca.name }) ); @@ -117,7 +116,6 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => { @@ -145,7 +143,6 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => { @@ -177,7 +174,6 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => { diff --git a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx index a6cff113a..7d424c1bc 100644 --- a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx +++ b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx @@ -67,7 +67,6 @@ export const PkiSyncRow = ({ const navigate = useNavigate(); const { id, - subscriberId, lastSyncMessage, destination, lastSyncedAt, @@ -117,7 +116,6 @@ export const PkiSyncRow = ({ const destinationDetails = PKI_SYNC_MAP[destination]; const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { - subscriberId: subscriberId || "", subscriberName: destinationName, name }); diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx index f87f8cdba..adb054149 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx @@ -53,7 +53,7 @@ type Props = { }; export const PkiSyncActionTriggers = ({ pkiSync }: Props) => { - const { destination, subscriberId, projectId, id } = pkiSync; + const { destination, projectId, id } = pkiSync; const navigate = useNavigate(); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ @@ -112,7 +112,6 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => { }, [updatePkiSyncMutation, id, projectId, pkiSync.isAutoSyncEnabled]); const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { - subscriberId: subscriberId || "", subscriberName: destinationName, name: pkiSync.name }); diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncCertificatesSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncCertificatesSection.tsx index 8a990bd4c..761f52cdd 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncCertificatesSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncCertificatesSection.tsx @@ -88,7 +88,6 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => { const destinationName = PKI_SYNC_MAP[pkiSync.destination].name; const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { - subscriberId: pkiSync.subscriberId || "", subscriberName: destinationName, name: pkiSync.name }); diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection.tsx index 22611171b..651b03403 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection.tsx @@ -31,7 +31,7 @@ type Props = { }; export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props) => { - const { destination, subscriberId } = pkiSync; + const { destination } = pkiSync; const destinationDetails = PKI_SYNC_MAP[destination]; @@ -55,7 +55,6 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props) } const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { - subscriberId: subscriberId || "", subscriberName: destinationDetails.name, name: pkiSync.name }); diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDetailsSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDetailsSection.tsx index 45fc9f371..5ebfc659b 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDetailsSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDetailsSection.tsx @@ -34,8 +34,7 @@ type Props = { }; export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => { - const { syncStatus, lastSyncMessage, lastSyncedAt, name, description, subscriberId, subscriber } = - pkiSync; + const { syncStatus, lastSyncMessage, lastSyncedAt, name, description, subscriber } = pkiSync; const failureMessage = useMemo(() => { if (syncStatus === PkiSyncStatus.Failed) { @@ -54,7 +53,6 @@ export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => { const destinationName = PKI_SYNC_MAP[pkiSync.destination].name; const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { - subscriberId: subscriber?.id || subscriberId || "", subscriberName: destinationName, name: pkiSync.name }); diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncFieldMappingsSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncFieldMappingsSection.tsx index c293f4982..41745bc86 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncFieldMappingsSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncFieldMappingsSection.tsx @@ -39,7 +39,6 @@ export const PkiSyncFieldMappingsSection = ({ pkiSync, onEditMappings }: Props) const destinationName = PKI_SYNC_MAP[pkiSync.destination].name; const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { - subscriberId: pkiSync.subscriberId || "", subscriberName: destinationName, name: pkiSync.name }); diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncOptionsSection/PkiSyncOptionsSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncOptionsSection/PkiSyncOptionsSection.tsx index 8cf57b874..891b4c3f0 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncOptionsSection/PkiSyncOptionsSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncOptionsSection/PkiSyncOptionsSection.tsx @@ -38,7 +38,6 @@ export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => { const destinationName = PKI_SYNC_MAP[pkiSync.destination].name; const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { - subscriberId: pkiSync.subscriberId || "", subscriberName: destinationName, name: pkiSync.name }); diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncSourceSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncSourceSection.tsx index 3097dd4c8..3abaee734 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncSourceSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncSourceSection.tsx @@ -30,7 +30,6 @@ export const PkiSyncSourceSection = ({ pkiSync, onEditSource }: Props) => { const destinationName = PKI_SYNC_MAP[pkiSync.destination].name; const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { - subscriberId: subscriberId || "", subscriberName: destinationName, name: pkiSync.name }); diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/CertificatePermissionConditions.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/CertificatePermissionConditions.tsx index 06eb5a8f9..7fa49de53 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/CertificatePermissionConditions.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/CertificatePermissionConditions.tsx @@ -14,7 +14,6 @@ export const CertificatePermissionConditions = ({ position = 0, isDisabled }: Pr subject={ProjectPermissionSub.Certificates} position={position} selectOptions={[ - { value: "id", label: "Certificate ID" }, { value: "commonName", label: "Common Name" }, { value: "altNames", label: "Subject Alternative Names" }, { value: "serialNumber", label: "Serial Number" } diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PermissionConditionHelpers.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PermissionConditionHelpers.tsx index f6c44b386..c8408d1cf 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PermissionConditionHelpers.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PermissionConditionHelpers.tsx @@ -36,6 +36,7 @@ export const renderOperatorSelectItems = (type: string) => { return ( <> Equal + Not Equal Glob Match In diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index 1178102f2..566a5f93a 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -57,7 +57,6 @@ const CertificatePolicyActionSchema = z.object({ [ProjectPermissionCertificateActions.Delete]: z.boolean().optional(), [ProjectPermissionCertificateActions.Edit]: z.boolean().optional(), [ProjectPermissionCertificateActions.Read]: z.boolean().optional(), - [ProjectPermissionCertificateActions.List]: z.boolean().optional(), [ProjectPermissionCertificateActions.ReadPrivateKey]: z.boolean().optional(), [ProjectPermissionCertificateActions.Import]: z.boolean().optional() }); @@ -67,7 +66,6 @@ const CertificateAuthorityPolicyActionSchema = z.object({ [ProjectPermissionCertificateAuthorityActions.Delete]: z.boolean().optional(), [ProjectPermissionCertificateAuthorityActions.Edit]: z.boolean().optional(), [ProjectPermissionCertificateAuthorityActions.Read]: z.boolean().optional(), - [ProjectPermissionCertificateAuthorityActions.List]: z.boolean().optional(), [ProjectPermissionCertificateAuthorityActions.Renew]: z.boolean().optional(), [ProjectPermissionCertificateAuthorityActions.SignIntermediate]: z.boolean().optional() }); @@ -123,7 +121,6 @@ const PkiSyncPolicyActionSchema = z.object({ [ProjectPermissionPkiSyncActions.Create]: z.boolean().optional(), [ProjectPermissionPkiSyncActions.Edit]: z.boolean().optional(), [ProjectPermissionPkiSyncActions.Delete]: z.boolean().optional(), - [ProjectPermissionPkiSyncActions.List]: z.boolean().optional(), [ProjectPermissionPkiSyncActions.SyncCertificates]: z.boolean().optional(), [ProjectPermissionPkiSyncActions.ImportCertificates]: z.boolean().optional(), [ProjectPermissionPkiSyncActions.RemoveCertificates]: z.boolean().optional() @@ -234,7 +231,6 @@ const PkiTemplatePolicyActionSchema = z.object({ }); const CertificateProfilePolicyActionSchema = z.object({ [ProjectPermissionCertificateProfileActions.Read]: z.boolean().optional(), - [ProjectPermissionCertificateProfileActions.List]: z.boolean().optional(), [ProjectPermissionCertificateProfileActions.Create]: z.boolean().optional(), [ProjectPermissionCertificateProfileActions.Edit]: z.boolean().optional(), [ProjectPermissionCertificateProfileActions.Delete]: z.boolean().optional(), @@ -696,7 +692,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { const canEdit = action.includes(ProjectPermissionPkiSyncActions.Edit); const canDelete = action.includes(ProjectPermissionPkiSyncActions.Delete); const canCreate = action.includes(ProjectPermissionPkiSyncActions.Create); - const canList = action.includes(ProjectPermissionPkiSyncActions.List); const canSyncCertificates = action.includes( ProjectPermissionPkiSyncActions.SyncCertificates ); @@ -715,7 +710,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { [ProjectPermissionPkiSyncActions.Create]: canCreate, [ProjectPermissionPkiSyncActions.Edit]: canEdit, [ProjectPermissionPkiSyncActions.Delete]: canDelete, - [ProjectPermissionPkiSyncActions.List]: canList, [ProjectPermissionPkiSyncActions.SyncCertificates]: canSyncCertificates, [ProjectPermissionPkiSyncActions.ImportCertificates]: canImportCertificates, [ProjectPermissionPkiSyncActions.RemoveCertificates]: canRemoveCertificates, @@ -864,7 +858,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { const canCreate = action.includes(ProjectPermissionCertificateAuthorityActions.Create); const canEdit = action.includes(ProjectPermissionCertificateAuthorityActions.Edit); const canDelete = action.includes(ProjectPermissionCertificateAuthorityActions.Delete); - const canList = action.includes(ProjectPermissionCertificateAuthorityActions.List); const canRenew = action.includes(ProjectPermissionCertificateAuthorityActions.Renew); const canSignIntermediate = action.includes( ProjectPermissionCertificateAuthorityActions.SignIntermediate @@ -876,7 +869,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { [ProjectPermissionCertificateAuthorityActions.Create]: canCreate, [ProjectPermissionCertificateAuthorityActions.Edit]: canEdit, [ProjectPermissionCertificateAuthorityActions.Delete]: canDelete, - [ProjectPermissionCertificateAuthorityActions.List]: canList, [ProjectPermissionCertificateAuthorityActions.Renew]: canRenew, [ProjectPermissionCertificateAuthorityActions.SignIntermediate]: canSignIntermediate, conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], @@ -937,7 +929,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { const canEdit = action.includes(ProjectPermissionCertificateActions.Edit); const canDelete = action.includes(ProjectPermissionCertificateActions.Delete); const canCreate = action.includes(ProjectPermissionCertificateActions.Create); - const canList = action.includes(ProjectPermissionCertificateActions.List); const canImport = action.includes(ProjectPermissionCertificateActions.Import); const canReadPrivateKey = action.includes(ProjectPermissionCertificateActions.ReadPrivateKey); @@ -949,7 +940,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { [ProjectPermissionCertificateActions.Edit]: canEdit, [ProjectPermissionCertificateActions.Create]: canCreate, [ProjectPermissionCertificateActions.Delete]: canDelete, - [ProjectPermissionCertificateActions.List]: canList, [ProjectPermissionCertificateActions.ReadPrivateKey]: canReadPrivateKey, [ProjectPermissionCertificateActions.Import]: canImport, conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], @@ -1250,9 +1240,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { [ProjectPermissionCertificateProfileActions.Read]: action.includes( ProjectPermissionCertificateProfileActions.Read ), - [ProjectPermissionCertificateProfileActions.List]: action.includes( - ProjectPermissionCertificateProfileActions.List - ), [ProjectPermissionCertificateProfileActions.IssueCert]: action.includes( ProjectPermissionCertificateProfileActions.IssueCert ), @@ -1629,7 +1616,6 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { title: "Certificate Authorities", actions: [ { label: "Read", value: ProjectPermissionCertificateAuthorityActions.Read }, - { label: "List", value: ProjectPermissionCertificateAuthorityActions.List }, { label: "Create", value: ProjectPermissionCertificateAuthorityActions.Create }, { label: "Modify", value: ProjectPermissionCertificateAuthorityActions.Edit }, { label: "Remove", value: ProjectPermissionCertificateAuthorityActions.Delete }, @@ -1644,7 +1630,6 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { title: "Certificates", actions: [ { label: "Read", value: ProjectPermissionCertificateActions.Read }, - { label: "List", value: ProjectPermissionCertificateActions.List }, { label: "Read Private Key", value: ProjectPermissionCertificateActions.ReadPrivateKey }, // { label: "Create", value: ProjectPermissionCertificateActions.Create }, // Hidden from UI - kept for backend compatibility { label: "Import", value: ProjectPermissionCertificateActions.Import }, @@ -1666,7 +1651,6 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { title: "Certificate Profiles", actions: [ { label: "Read", value: ProjectPermissionCertificateProfileActions.Read }, - { label: "List", value: ProjectPermissionCertificateProfileActions.List }, { label: "Create", value: ProjectPermissionCertificateProfileActions.Create }, { label: "Modify", value: ProjectPermissionCertificateProfileActions.Edit }, { label: "Remove", value: ProjectPermissionCertificateProfileActions.Delete }, @@ -1799,7 +1783,6 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { title: "Certificate Syncs", actions: [ { label: "Read", value: ProjectPermissionPkiSyncActions.Read }, - { label: "List", value: ProjectPermissionPkiSyncActions.List }, { label: "Create", value: ProjectPermissionPkiSyncActions.Create }, { label: "Modify", value: ProjectPermissionPkiSyncActions.Edit }, { label: "Remove", value: ProjectPermissionPkiSyncActions.Delete }, From 3f73f7cb05ab9b0bda1c7dab4be9a44758a14446 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Thu, 4 Dec 2025 10:53:39 -0300 Subject: [PATCH 5/9] Fix unit test --- .../src/lib/casl/permission-filter-utils.ts | 2 +- .../certificate-profile-service.test.ts | 41 ++++++++++------ .../certificate-template-v2-service.test.ts | 48 ++++++++++++------- 3 files changed, 59 insertions(+), 32 deletions(-) diff --git a/backend/src/lib/casl/permission-filter-utils.ts b/backend/src/lib/casl/permission-filter-utils.ts index 06dabe0a0..1f074d1d6 100644 --- a/backend/src/lib/casl/permission-filter-utils.ts +++ b/backend/src/lib/casl/permission-filter-utils.ts @@ -203,7 +203,7 @@ export function convertConditionToDbFilter(condition: Record): switch (operator) { case "$glob": { // Convert "*.es" glob → regex /^.*\.es$/i - const regex = new RegExp(`^${String(opValue).replace(/\*/g, ".*")}$`, "i"); + const regex = new RE2(`^${String(opValue).replace(/\*/g, ".*")}$`, "i"); dbFilter[key] = { $regex: regex }; break; } diff --git a/backend/src/services/certificate-profile/certificate-profile-service.test.ts b/backend/src/services/certificate-profile/certificate-profile-service.test.ts index 17ddcaa30..0bc7a320b 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.test.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.test.ts @@ -169,7 +169,8 @@ describe("CertificateProfileService", () => { const mockPermissionService = { getProjectPermission: vi.fn().mockResolvedValue({ permission: { - throwUnlessCan: vi.fn() + throwUnlessCan: vi.fn(), + rules: [] } }) } as unknown as Pick; @@ -598,13 +599,18 @@ describe("CertificateProfileService", () => { expect(result.profiles).toEqual(mockProfiles); expect(result.totalCount).toBe(1); - expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith("project-123", { - offset: 0, - limit: 20, - search: undefined, - enrollmentType: undefined, - caId: undefined - }); + expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith( + "project-123", + { + offset: 0, + limit: 20, + search: undefined, + enrollmentType: undefined, + caId: undefined, + issuerType: undefined + }, + {} + ); }); it("should list profiles with filters", async () => { @@ -618,13 +624,18 @@ describe("CertificateProfileService", () => { caId: "ca-123" }); - expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith("project-123", { - offset: 10, - limit: 5, - search: "test", - enrollmentType: EnrollmentType.API, - caId: "ca-123" - }); + expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith( + "project-123", + { + offset: 10, + limit: 5, + search: "test", + enrollmentType: EnrollmentType.API, + caId: "ca-123", + issuerType: undefined + }, + {} + ); }); }); diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts index f73d516a6..636bd4be7 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts @@ -267,14 +267,22 @@ describe("CertificateTemplateV2Service", () => { limit: 20 }); - expect(mockCertificateTemplateV2DAL.findByProjectId).toHaveBeenCalledWith("project-123", { - offset: 0, - limit: 20, - search: undefined - }); - expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith("project-123", { - search: undefined - }); + expect(mockCertificateTemplateV2DAL.findByProjectId).toHaveBeenCalledWith( + "project-123", + { + offset: 0, + limit: 20, + search: undefined + }, + {} + ); + expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith( + "project-123", + { + search: undefined + }, + {} + ); expect(result).toEqual({ templates, totalCount }); }); @@ -291,14 +299,22 @@ describe("CertificateTemplateV2Service", () => { search: "web server" }); - expect(mockCertificateTemplateV2DAL.findByProjectId).toHaveBeenCalledWith("project-123", { - offset: 0, - limit: 20, - search: "web server" - }); - expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith("project-123", { - search: "web server" - }); + expect(mockCertificateTemplateV2DAL.findByProjectId).toHaveBeenCalledWith( + "project-123", + { + offset: 0, + limit: 20, + search: "web server" + }, + {} + ); + expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith( + "project-123", + { + search: "web server" + }, + {} + ); }); }); From 2b038575adb2b9a1f1cc72bd91e97f702d7db05c Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Thu, 4 Dec 2025 12:17:59 -0300 Subject: [PATCH 6/9] Remove unused ca.id on CertificateAuthorities permission --- .../certificate-authority/certificate-authority-service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 4be0de84c..4e2ac1aba 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -736,7 +736,6 @@ export const certificateAuthorityServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateAuthorityActions.Read, subject(ProjectPermissionSub.CertificateAuthorities, { - caId: ca.id, name: ca.name }) ); From 003aa1cd59fae7f9a91a66bcc3ad709dd0ca8865 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Thu, 4 Dec 2025 14:18:10 -0300 Subject: [PATCH 7/9] Remove duplicate read permission on default roles --- backend/src/ee/services/permission/default-roles.ts | 6 ++---- .../acme/acme-certificate-authority-fns.ts | 3 ++- .../azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts | 3 ++- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index 4151face4..7b2e0ae00 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -109,8 +109,7 @@ const buildAdminPermissionRules = () => { ProjectPermissionCertificateActions.Create, ProjectPermissionCertificateActions.Delete, ProjectPermissionCertificateActions.ReadPrivateKey, - ProjectPermissionCertificateActions.Import, - ProjectPermissionCertificateActions.Read + ProjectPermissionCertificateActions.Import ], ProjectPermissionSub.Certificates ); @@ -484,8 +483,7 @@ const buildMemberPermissionRules = () => { ProjectPermissionCertificateActions.Edit, ProjectPermissionCertificateActions.Create, ProjectPermissionCertificateActions.Delete, - ProjectPermissionCertificateActions.Import, - ProjectPermissionCertificateActions.Read + ProjectPermissionCertificateActions.Import ], ProjectPermissionSub.Certificates ); diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts index 6ef479971..0f39935ec 100644 --- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts @@ -7,6 +7,7 @@ import { TableName } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, CryptographyError, NotFoundError } from "@app/lib/errors"; +import { PermissionFilters } from "@app/lib/knex/permission-filter-utils"; import { OrgServiceActor } from "@app/lib/types"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; @@ -720,7 +721,7 @@ export const AcmeCertificateAuthorityFns = ({ permissionFilters }: { projectId: string; - permissionFilters?: import("@app/lib/knex/permission-filter-utils").PermissionFilters; + permissionFilters?: PermissionFilters; }) => { const cas = await certificateAuthorityDAL.findWithAssociatedCa( { diff --git a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts index eea877b6e..92be70560 100644 --- a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts @@ -5,6 +5,7 @@ import RE2 from "re2"; import { TableName } from "@app/db/schemas"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { PermissionFilters } from "@app/lib/knex/permission-filter-utils"; import { ms } from "@app/lib/ms"; import { OrgServiceActor } from "@app/lib/types"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; @@ -803,7 +804,7 @@ export const AzureAdCsCertificateAuthorityFns = ({ permissionFilters }: { projectId: string; - permissionFilters?: import("@app/lib/knex/permission-filter-utils").PermissionFilters; + permissionFilters?: PermissionFilters; }) => { const cas = await certificateAuthorityDAL.findWithAssociatedCa( { From 90c73def68f098a22dc43101b0cf62aecb32de38 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Thu, 4 Dec 2025 18:55:26 -0300 Subject: [PATCH 8/9] UI improvements on PKI product --- .../src/lib/casl/permission-filter-utils.ts | 118 +-------------- .../src/lib/knex/permission-filter-utils.ts | 142 +----------------- .../server/routes/v1/certificate-router.ts | 3 +- .../acme/acme-certificate-authority-fns.ts | 4 +- .../azure-ad-cs-certificate-authority-fns.ts | 4 +- .../certificate-authority-dal.ts | 13 +- .../certificate-authority-service.ts | 38 ++--- .../certificate-profile-dal.ts | 21 +-- .../certificate-profile-service.test.ts | 4 +- .../certificate-profile-service.ts | 8 +- .../certificate-request-service.test.ts | 5 +- .../certificate-request-service.ts | 5 +- .../certificate-template-v2-dal.ts | 21 +-- .../certificate-template-v2-service.test.ts | 8 +- .../certificate-template-v2-service.ts | 8 +- .../services/certificate/certificate-dal.ts | 13 +- backend/src/services/pki-sync/pki-sync-dal.ts | 17 ++- .../src/services/pki-sync/pki-sync-service.ts | 6 +- .../src/services/project/project-service.ts | 14 +- .../ProjectPermissionContext/index.tsx | 1 + frontend/src/context/index.tsx | 1 + .../components/ExternalCaTable.tsx | 10 +- .../components/CertificatesSection.tsx | 26 ++-- .../components/CertificatesTable.tsx | 131 +++++++++------- .../PoliciesPage/PoliciesPage.tsx | 33 +++- .../CreateTemplateModal.tsx | 7 +- 26 files changed, 243 insertions(+), 418 deletions(-) diff --git a/backend/src/lib/casl/permission-filter-utils.ts b/backend/src/lib/casl/permission-filter-utils.ts index 1f074d1d6..35f519c33 100644 --- a/backend/src/lib/casl/permission-filter-utils.ts +++ b/backend/src/lib/casl/permission-filter-utils.ts @@ -41,7 +41,7 @@ interface MongoGlobFilter { * @param isInverted - Whether this rule is inverted (forbidden) * @returns Record of field names to arrays of filter configurations */ -export const buildPermissionFiltersFromConditions = (conditions: MongoQuery, isInverted = false): PermissionFilters => { +const buildPermissionFiltersFromConditions = (conditions: MongoQuery, isInverted = false): PermissionFilters => { const permissionFilters: PermissionFilters = {}; function addFilterToField(key: string, operator: string, value: unknown, isPattern: boolean) { @@ -184,83 +184,6 @@ export const buildPermissionFiltersFromConditions = (conditions: MongoQuery, isI return permissionFilters; }; -/** - * Convert CASL condition object to MongoDB-compatible database filter - * @param condition - CASL condition object - * @returns MongoDB filter object - */ -export function convertConditionToDbFilter(condition: Record): Record { - const dbFilter: Record = {}; - - for (const key in condition) { - if (Object.prototype.hasOwnProperty.call(condition, key)) { - const value = condition[key]; - - if (value && typeof value === "object" && !Array.isArray(value)) { - const operator = Object.keys(value)[0]; - const opValue = (value as Record)[operator]; - - switch (operator) { - case "$glob": { - // Convert "*.es" glob → regex /^.*\.es$/i - const regex = new RE2(`^${String(opValue).replace(/\*/g, ".*")}$`, "i"); - dbFilter[key] = { $regex: regex }; - break; - } - - case "$in": - dbFilter[key] = { $in: opValue }; - break; - - case "$eq": - dbFilter[key] = { $eq: opValue }; - break; - - default: - dbFilter[key] = value; - break; - } - } else { - dbFilter[key] = { $eq: value }; - } - } - } - - return dbFilter; -} - -/** - * Extract ALL conditions for a subject and action, - * converting them into DB-safe filter objects. - * @param ability - CASL MongoAbility instance - * @param action - Permission action to filter for - * @param subjectName - Permission subject to filter for - * @returns MongoDB filter object with $or conditions - */ -export function getDbFiltersForAbility( - ability: MongoAbility, - action: string, - subjectName: string -): Record { - const matchingRules = ability.rules.filter((rule: RawRuleOf) => { - const actionMatches = Array.isArray(rule.action) ? rule.action.includes(action) : rule.action === action; - const subjectMatches = Array.isArray(rule.subject) - ? rule.subject.includes(subjectName) - : rule.subject === subjectName; - return actionMatches && subjectMatches && rule.conditions; - }); - - const converted = matchingRules.map((rule: RawRuleOf) => - convertConditionToDbFilter(rule.conditions as Record) - ); - - if (converted.length === 0) { - return {}; - } - - return { $or: converted }; -} - /** * Extract permission filters for a subject and action, * converting them into ProcessedPermissionRules format for use with Knex queries. @@ -300,42 +223,3 @@ export function getProcessedPermissionRules( return { allowRules, forbidRules }; } - -/** - * Extract permission filters for a subject and action, - * converting them into PermissionFilters format for use with Knex queries. - * This is a simplified version that maintains backward compatibility. - * @param ability - CASL MongoAbility instance - * @param action - Permission action to filter for - * @param subjectName - Permission subject to filter for - * @returns PermissionFilters object for use with applyPermissionFiltersToQuery - */ -export function getPermissionFiltersForAbility( - ability: MongoAbility, - action: string, - subjectName: string -): PermissionFilters { - const processedRules = getProcessedPermissionRules(ability, action, subjectName); - - const mergedFilters: PermissionFilters = {}; - - processedRules.allowRules.forEach((rule) => { - Object.entries(rule).forEach(([key, filterConfigs]) => { - if (!mergedFilters[key]) { - mergedFilters[key] = []; - } - mergedFilters[key].push(...filterConfigs); - }); - }); - - processedRules.forbidRules.forEach((rule) => { - Object.entries(rule).forEach(([key, filterConfigs]) => { - if (!mergedFilters[key]) { - mergedFilters[key] = []; - } - mergedFilters[key].push(...filterConfigs); - }); - }); - - return mergedFilters; -} diff --git a/backend/src/lib/knex/permission-filter-utils.ts b/backend/src/lib/knex/permission-filter-utils.ts index 3b8f448ec..d6e0f92a4 100644 --- a/backend/src/lib/knex/permission-filter-utils.ts +++ b/backend/src/lib/knex/permission-filter-utils.ts @@ -15,86 +15,6 @@ export interface ProcessedPermissionRules { forbidRules: Array>>; } -/** - * Applies permission filters to a Knex query for any table - * @param query - The Knex query builder instance - * @param tableName - The name of the table to apply filters to - * @param permissionFilters - Record of field names to arrays of filter configurations - * @returns The modified query builder with permission filters applied - */ -export const applyPermissionFiltersToQuery = ( - originalQuery: Knex.QueryBuilder, - tableName: string, - permissionFilters?: PermissionFilters -): Knex.QueryBuilder => { - if (!permissionFilters) { - return originalQuery; - } - - let query = originalQuery; - - Object.entries(permissionFilters).forEach(([key, filterConfigs]) => { - filterConfigs.forEach((filterConfig) => { - if (filterConfig.value !== undefined && filterConfig.value !== null) { - const { operator, value, isPattern } = filterConfig; - const fieldName = `${tableName}.${key}`; - - switch (operator) { - case "=": - query = query.andWhere(fieldName, "=", value as string | number); - break; - case "!=": - query = query.andWhere(fieldName, "!=", value as string | number); - break; - case "LIKE": { - const likePattern = isPattern ? String(value).replace(new RE2("\\*", "g"), "%") : String(value); - query = query.andWhere(fieldName, "like", likePattern); - break; - } - case "NOT LIKE": { - const notLikePattern = isPattern ? String(value).replace(new RE2("\\*", "g"), "%") : String(value); - query = query.andWhere(fieldName, "not like", notLikePattern); - break; - } - case "IN": { - const inValues = Array.isArray(value) ? value : [value]; - query = query.andWhere(fieldName, "in", inValues as (string | number)[]); - break; - } - case "NOT IN": { - const notInValues = Array.isArray(value) ? value : [value]; - query = query.andWhere(fieldName, "not in", notInValues as (string | number)[]); - break; - } - case ">": - query = query.andWhere(fieldName, ">", value as string | number); - break; - case ">=": - query = query.andWhere(fieldName, ">=", value as string | number); - break; - case "<": - query = query.andWhere(fieldName, "<", value as string | number); - break; - case "<=": - query = query.andWhere(fieldName, "<=", value as string | number); - break; - case "IS NULL": - query = query.andWhere(fieldName, "is", null); - break; - case "IS NOT NULL": - query = query.andWhere(fieldName, "is not", null); - break; - default: - query = query.andWhere(fieldName, "=", value as string | number); - break; - } - } - }); - }); - - return query; -}; - /** * Applies a single filter configuration to a query * @param query - The Knex query builder instance @@ -204,65 +124,9 @@ export const applyProcessedPermissionRulesToQuery = ( if (processedRules.forbidRules.length > 0) { processedRules.forbidRules.forEach((forbidRule) => { - query = query.andWhere((forbidBuilder) => { - let hasConditions = false; - Object.entries(forbidRule).forEach(([key, filterConfigs]) => { - filterConfigs.forEach((filterConfig) => { - const negatedConfig = { ...filterConfig }; - - switch (filterConfig.operator) { - case "=": - negatedConfig.operator = "!="; - break; - case "!=": - negatedConfig.operator = "="; - break; - case "LIKE": - negatedConfig.operator = "NOT LIKE"; - break; - case "NOT LIKE": - negatedConfig.operator = "LIKE"; - break; - case "IN": - negatedConfig.operator = "NOT IN"; - break; - case "NOT IN": - negatedConfig.operator = "IN"; - break; - case ">": - negatedConfig.operator = "<="; - break; - case ">=": - negatedConfig.operator = "<"; - break; - case "<": - negatedConfig.operator = ">="; - break; - case "<=": - negatedConfig.operator = ">"; - break; - case "IS NULL": - negatedConfig.operator = "IS NOT NULL"; - break; - case "IS NOT NULL": - negatedConfig.operator = "IS NULL"; - break; - default: - negatedConfig.operator = "!="; - break; - } - - if (hasConditions) { - void forbidBuilder.orWhere((subBuilder) => { - applySingleFilter(subBuilder, tableName, key, negatedConfig); - }); - } else { - void forbidBuilder.where((subBuilder) => { - applySingleFilter(subBuilder, tableName, key, negatedConfig); - }); - hasConditions = true; - } - }); + Object.entries(forbidRule).forEach(([key, filterConfigs]) => { + filterConfigs.forEach((filterConfig) => { + applySingleFilter(query, tableName, key, filterConfig); }); }); }); diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index f14ff437f..397085171 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -176,7 +176,8 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId + actorOrgId: req.permission.orgId, + isInternal: true }); const caType = (ca?.externalCa?.type as CaType) ?? CaType.INTERNAL; useOrderFlow = caType !== CaType.INTERNAL; diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts index 0f39935ec..9616b3090 100644 --- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts @@ -7,7 +7,7 @@ import { TableName } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, CryptographyError, NotFoundError } from "@app/lib/errors"; -import { PermissionFilters } from "@app/lib/knex/permission-filter-utils"; +import { ProcessedPermissionRules } from "@app/lib/knex/permission-filter-utils"; import { OrgServiceActor } from "@app/lib/types"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; @@ -721,7 +721,7 @@ export const AcmeCertificateAuthorityFns = ({ permissionFilters }: { projectId: string; - permissionFilters?: PermissionFilters; + permissionFilters?: ProcessedPermissionRules; }) => { const cas = await certificateAuthorityDAL.findWithAssociatedCa( { diff --git a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts index 92be70560..7ebd66146 100644 --- a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts @@ -5,7 +5,7 @@ import RE2 from "re2"; import { TableName } from "@app/db/schemas"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; -import { PermissionFilters } from "@app/lib/knex/permission-filter-utils"; +import { ProcessedPermissionRules } from "@app/lib/knex/permission-filter-utils"; import { ms } from "@app/lib/ms"; import { OrgServiceActor } from "@app/lib/types"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; @@ -804,7 +804,7 @@ export const AzureAdCsCertificateAuthorityFns = ({ permissionFilters }: { projectId: string; - permissionFilters?: PermissionFilters; + permissionFilters?: ProcessedPermissionRules; }) => { const cas = await certificateAuthorityDAL.findWithAssociatedCa( { diff --git a/backend/src/services/certificate-authority/certificate-authority-dal.ts b/backend/src/services/certificate-authority/certificate-authority-dal.ts index 5809b04d9..6f621a515 100644 --- a/backend/src/services/certificate-authority/certificate-authority-dal.ts +++ b/backend/src/services/certificate-authority/certificate-authority-dal.ts @@ -4,7 +4,10 @@ import { TDbClient } from "@app/db"; import { CertificateAuthoritiesSchema, TableName, TCertificateAuthorities } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { buildFindFilter, ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex"; -import { applyPermissionFiltersToQuery, type PermissionFilters } from "@app/lib/knex/permission-filter-utils"; +import { + applyProcessedPermissionRulesToQuery, + type ProcessedPermissionRules +} from "@app/lib/knex/permission-filter-utils"; export type TCertificateAuthorityDALFactory = ReturnType; @@ -221,7 +224,7 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { const findWithAssociatedCa = async ( filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string; serialNumber?: string }, { offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt = {}, - permissionFilters?: PermissionFilters, + permissionFilters?: ProcessedPermissionRules, tx?: Knex ) => { try { @@ -271,7 +274,11 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { ); if (permissionFilters) { - query = applyPermissionFiltersToQuery(query, TableName.CertificateAuthority, permissionFilters) as typeof query; + query = applyProcessedPermissionRulesToQuery( + query, + TableName.CertificateAuthority, + permissionFilters + ) as typeof query; } if (limit) void query.limit(limit); diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 4e2ac1aba..a6dd4ce9e 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -6,7 +6,7 @@ import { ProjectPermissionCertificateAuthorityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { getPermissionFiltersForAbility } from "@app/lib/casl/permission-filter-utils"; +import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; @@ -337,7 +337,7 @@ export const certificateAuthorityServiceFactory = ({ ProjectPermissionSub.CertificateAuthorities ); - const permissionFilters = getPermissionFiltersForAbility( + const permissionFilters = getProcessedPermissionRules( permission, ProjectPermissionCertificateAuthorityActions.Read, ProjectPermissionSub.CertificateAuthorities @@ -711,34 +711,38 @@ export const certificateAuthorityServiceFactory = ({ actor, actorId, actorAuthMethod, - actorOrgId + actorOrgId, + isInternal }: { caId: string; actor: OrgServiceActor["type"]; actorId: string; actorAuthMethod: OrgServiceActor["authMethod"]; actorOrgId?: string; + isInternal?: boolean; }) => { const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); if (!ca) { throw new NotFoundError({ message: "CA not found" }); } - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: ca.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); + if (!isInternal) { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateAuthorityActions.Read, - subject(ProjectPermissionSub.CertificateAuthorities, { - name: ca.name - }) - ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateAuthorityActions.Read, + subject(ProjectPermissionSub.CertificateAuthorities, { + name: ca.name + }) + ); + } return ca; }; diff --git a/backend/src/services/certificate-profile/certificate-profile-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index 5bab780eb..3a8f99f1a 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -4,7 +4,10 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; -import { applyPermissionFiltersToQuery, type PermissionFilters } from "@app/lib/knex/permission-filter-utils"; +import { + applyProcessedPermissionRulesToQuery, + type ProcessedPermissionRules +} from "@app/lib/knex/permission-filter-utils"; import { EnrollmentType, @@ -277,7 +280,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { issuerType?: IssuerType; caId?: string; } = {}, - permissionFilters?: PermissionFilters, + processedRules?: ProcessedPermissionRules, tx?: Knex ): Promise => { try { @@ -356,11 +359,11 @@ export const certificateProfileDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeId") ); - if (permissionFilters) { - query = applyPermissionFiltersToQuery( + if (processedRules) { + query = applyProcessedPermissionRulesToQuery( query, TableName.PkiCertificateProfile, - permissionFilters + processedRules ) as typeof query; } @@ -442,7 +445,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { issuerType?: IssuerType; caId?: string; } = {}, - permissionFilters?: PermissionFilters, + processedRules?: ProcessedPermissionRules, tx?: Knex ): Promise => { try { @@ -470,11 +473,11 @@ export const certificateProfileDALFactory = (db: TDbClient) => { query = query.where({ issuerType }); } - if (permissionFilters) { - query = applyPermissionFiltersToQuery( + if (processedRules) { + query = applyProcessedPermissionRulesToQuery( query, TableName.PkiCertificateProfile, - permissionFilters + processedRules ) as typeof query; } diff --git a/backend/src/services/certificate-profile/certificate-profile-service.test.ts b/backend/src/services/certificate-profile/certificate-profile-service.test.ts index 0bc7a320b..d10188e6f 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.test.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.test.ts @@ -609,7 +609,7 @@ describe("CertificateProfileService", () => { caId: undefined, issuerType: undefined }, - {} + { allowRules: [], forbidRules: [] } ); }); @@ -634,7 +634,7 @@ describe("CertificateProfileService", () => { caId: "ca-123", issuerType: undefined }, - {} + { allowRules: [], forbidRules: [] } ); }); }); diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index 20da7891a..59e3afbd9 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -10,7 +10,7 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { buildUrl } from "@app/ee/services/pki-acme/pki-acme-fns"; -import { getPermissionFiltersForAbility } from "@app/lib/casl/permission-filter-utils"; +import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils"; import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; @@ -750,7 +750,7 @@ export const certificateProfileServiceFactory = ({ ProjectPermissionSub.CertificateProfiles ); - const permissionFilters = getPermissionFiltersForAbility( + const processedRules = getProcessedPermissionRules( permission, ProjectPermissionCertificateProfileActions.Read, ProjectPermissionSub.CertificateProfiles @@ -766,7 +766,7 @@ export const certificateProfileServiceFactory = ({ issuerType, caId }, - permissionFilters + processedRules ); const totalCount = await certificateProfileDAL.countByProjectId( @@ -777,7 +777,7 @@ export const certificateProfileServiceFactory = ({ issuerType, caId }, - permissionFilters + processedRules ); const convertedProfiles = await Promise.all( diff --git a/backend/src/services/certificate-request/certificate-request-service.test.ts b/backend/src/services/certificate-request/certificate-request-service.test.ts index 5e6b870bf..66d5c613b 100644 --- a/backend/src/services/certificate-request/certificate-request-service.test.ts +++ b/backend/src/services/certificate-request/certificate-request-service.test.ts @@ -10,6 +10,7 @@ import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionCertificateActions, + ProjectPermissionCertificateProfileActions, ProjectPermissionSet, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -79,8 +80,8 @@ describe("CertificateRequestService", () => { const mockPermission = { permission: createMongoAbility([ { - action: ProjectPermissionCertificateActions.Create, - subject: ProjectPermissionSub.Certificates + action: ProjectPermissionCertificateProfileActions.IssueCert, + subject: ProjectPermissionSub.CertificateProfiles } ]) }; diff --git a/backend/src/services/certificate-request/certificate-request-service.ts b/backend/src/services/certificate-request/certificate-request-service.ts index 15450ee6e..71129aba7 100644 --- a/backend/src/services/certificate-request/certificate-request-service.ts +++ b/backend/src/services/certificate-request/certificate-request-service.ts @@ -6,6 +6,7 @@ import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionCertificateActions, + ProjectPermissionCertificateProfileActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -110,8 +111,8 @@ export const certificateRequestServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateActions.Create, - ProjectPermissionSub.Certificates + ProjectPermissionCertificateProfileActions.IssueCert, + ProjectPermissionSub.CertificateProfiles ); } diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts index a2ed17a4c..4951986b1 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts @@ -5,7 +5,10 @@ import { TableName } from "@app/db/schemas"; import { TPkiCertificateTemplatesV2Insert } from "@app/db/schemas/pki-certificate-templates-v2"; import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; -import { applyPermissionFiltersToQuery, type PermissionFilters } from "@app/lib/knex/permission-filter-utils"; +import { + applyProcessedPermissionRulesToQuery, + type ProcessedPermissionRules +} from "@app/lib/knex/permission-filter-utils"; import { TCertificateTemplateV2, @@ -134,7 +137,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { limit?: number; search?: string; } = {}, - permissionFilters?: PermissionFilters, + processedRules?: ProcessedPermissionRules, tx?: Knex ) => { try { @@ -148,11 +151,11 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { }); } - if (permissionFilters) { - query = applyPermissionFiltersToQuery( + if (processedRules) { + query = applyProcessedPermissionRulesToQuery( query, TableName.PkiCertificateTemplateV2, - permissionFilters + processedRules ) as typeof query; } @@ -169,7 +172,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { options: { search?: string; } = {}, - permissionFilters?: PermissionFilters, + processedRules?: ProcessedPermissionRules, tx?: Knex ) => { try { @@ -183,11 +186,11 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { }); } - if (permissionFilters) { - query = applyPermissionFiltersToQuery( + if (processedRules) { + query = applyProcessedPermissionRulesToQuery( query, TableName.PkiCertificateTemplateV2, - permissionFilters + processedRules ) as typeof query; } diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts index 636bd4be7..3b8f7bc13 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts @@ -274,14 +274,14 @@ describe("CertificateTemplateV2Service", () => { limit: 20, search: undefined }, - {} + { allowRules: [], forbidRules: [] } ); expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith( "project-123", { search: undefined }, - {} + { allowRules: [], forbidRules: [] } ); expect(result).toEqual({ templates, totalCount }); }); @@ -306,14 +306,14 @@ describe("CertificateTemplateV2Service", () => { limit: 20, search: "web server" }, - {} + { allowRules: [], forbidRules: [] } ); expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith( "project-123", { search: "web server" }, - {} + { allowRules: [], forbidRules: [] } ); }); }); diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts index 36d77e4ff..5d24e4b8b 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts @@ -8,7 +8,7 @@ import { ProjectPermissionPkiTemplateActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { getPermissionFiltersForAbility } from "@app/lib/casl/permission-filter-utils"; +import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -872,7 +872,7 @@ export const certificateTemplateV2ServiceFactory = ({ ProjectPermissionSub.CertificateTemplates ); - const permissionFilters = getPermissionFiltersForAbility( + const processedRules = getProcessedPermissionRules( permission, ProjectPermissionPkiTemplateActions.Read, ProjectPermissionSub.CertificateTemplates @@ -880,10 +880,10 @@ export const certificateTemplateV2ServiceFactory = ({ const templates = await certificateTemplateV2DAL.findByProjectId( projectId, { offset, limit, search }, - permissionFilters + processedRules ); - const totalCount = await certificateTemplateV2DAL.countByProjectId(projectId, { search }, permissionFilters); + const totalCount = await certificateTemplateV2DAL.countByProjectId(projectId, { search }, processedRules); return { templates, diff --git a/backend/src/services/certificate/certificate-dal.ts b/backend/src/services/certificate/certificate-dal.ts index f61c46c28..72cef90fa 100644 --- a/backend/src/services/certificate/certificate-dal.ts +++ b/backend/src/services/certificate/certificate-dal.ts @@ -4,7 +4,10 @@ import { TDbClient } from "@app/db"; import { TableName, TCertificates } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; -import { applyPermissionFiltersToQuery, type PermissionFilters } from "@app/lib/knex/permission-filter-utils"; +import { + applyProcessedPermissionRulesToQuery, + type ProcessedPermissionRules +} from "@app/lib/knex/permission-filter-utils"; import { CertStatus } from "./certificate-types"; @@ -142,7 +145,7 @@ export const certificateDALFactory = (db: TDbClient) => { const findActiveCertificatesForSync = async ( filter: Partial, options?: { limit?: number; offset?: number }, - permissionFilters?: PermissionFilters + permissionFilters?: ProcessedPermissionRules ): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => { try { let query = db @@ -166,7 +169,7 @@ export const certificateDALFactory = (db: TDbClient) => { }); if (permissionFilters) { - query = applyPermissionFiltersToQuery(query, TableName.Certificate, permissionFilters) as typeof query; + query = applyProcessedPermissionRulesToQuery(query, TableName.Certificate, permissionFilters) as typeof query; } if (options?.offset) { @@ -274,7 +277,7 @@ export const certificateDALFactory = (db: TDbClient) => { const findWithPrivateKeyInfo = async ( filter: Partial, options?: { offset?: number; limit?: number; sort?: [string, "asc" | "desc"][] }, - permissionFilters?: PermissionFilters + permissionFilters?: ProcessedPermissionRules ): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => { try { let query = db @@ -295,7 +298,7 @@ export const certificateDALFactory = (db: TDbClient) => { }); if (permissionFilters) { - query = applyPermissionFiltersToQuery(query, TableName.Certificate, permissionFilters) as typeof query; + query = applyProcessedPermissionRulesToQuery(query, TableName.Certificate, permissionFilters) as typeof query; } if (options?.offset) { diff --git a/backend/src/services/pki-sync/pki-sync-dal.ts b/backend/src/services/pki-sync/pki-sync-dal.ts index 294ebff80..d04344b1e 100644 --- a/backend/src/services/pki-sync/pki-sync-dal.ts +++ b/backend/src/services/pki-sync/pki-sync-dal.ts @@ -4,7 +4,10 @@ import { TDbClient } from "@app/db"; import { TableName, TPkiSyncs } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex"; -import { applyPermissionFiltersToQuery, type PermissionFilters } from "@app/lib/knex/permission-filter-utils"; +import { + applyProcessedPermissionRulesToQuery, + type ProcessedPermissionRules +} from "@app/lib/knex/permission-filter-utils"; import { PkiSync } from "./pki-sync-enums"; @@ -47,12 +50,12 @@ const basePkiSyncWithSubscriberQuery = ({ filter, db, tx, - permissionFilters + processedRules }: { db: TDbClient; filter?: PkiSyncFindFilter; tx?: Knex; - permissionFilters?: PermissionFilters; + processedRules?: ProcessedPermissionRules; }) => { let query = (tx || db.replicaNode())(TableName.PkiSync) .leftJoin(TableName.AppConnection, `${TableName.PkiSync}.connectionId`, `${TableName.AppConnection}.id`) @@ -85,8 +88,8 @@ const basePkiSyncWithSubscriberQuery = ({ void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.PkiSync, filter))); } - if (permissionFilters) { - query = applyPermissionFiltersToQuery(query, TableName.PkiSync, permissionFilters) as typeof query; + if (processedRules) { + query = applyProcessedPermissionRulesToQuery(query, TableName.PkiSync, processedRules) as typeof query; } return query; @@ -193,7 +196,7 @@ export const pkiSyncDALFactory = (db: TDbClient) => { const findByProjectIdWithSubscribers = async ( projectId: string, - permissionFilters?: PermissionFilters, + processedRules?: ProcessedPermissionRules, tx?: Knex ) => { try { @@ -201,7 +204,7 @@ export const pkiSyncDALFactory = (db: TDbClient) => { filter: { projectId }, db, tx, - permissionFilters + processedRules }); return pkiSyncs.map(expandPkiSyncWithSubscriber); } catch (error) { diff --git a/backend/src/services/pki-sync/pki-sync-service.ts b/backend/src/services/pki-sync/pki-sync-service.ts index 2a4044208..f5e574213 100644 --- a/backend/src/services/pki-sync/pki-sync-service.ts +++ b/backend/src/services/pki-sync/pki-sync-service.ts @@ -4,7 +4,7 @@ import { ActionProjectType, TCertificateSyncs } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionPkiSyncActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { getPermissionFiltersForAbility } from "@app/lib/casl/permission-filter-utils"; +import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; @@ -358,13 +358,13 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs); - const permissionFilters = getPermissionFiltersForAbility( + const processedRules = getProcessedPermissionRules( permission, ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs ); - const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId, permissionFilters); + const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId, processedRules); if (certificateId) { const syncsWithCertificateInfo = await Promise.all( diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 2ea6503eb..cf1771aba 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -40,7 +40,7 @@ import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certific import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal"; import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal"; import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; -import { buildPermissionFiltersFromConditions, getDbFiltersForAbility } from "@app/lib/casl/permission-filter-utils"; +import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { DatabaseErrorCode } from "@app/lib/error-codes"; @@ -965,18 +965,16 @@ export const projectServiceFactory = ({ ProjectPermissionSub.Certificates ); - const filters = getDbFiltersForAbility( - permission, - ProjectPermissionCertificateActions.Read, - ProjectPermissionSub.Certificates - ); - const regularFilters = { projectId, ...(friendlyName && { friendlyName }), ...(commonName && { commonName }) }; - const permissionFilters = buildPermissionFiltersFromConditions(filters || {}); + const permissionFilters = getProcessedPermissionRules( + permission, + ProjectPermissionCertificateActions.Read, + ProjectPermissionSub.Certificates + ); const certificates = forPkiSync ? await certificateDAL.findActiveCertificatesForSync(regularFilters, { offset, limit }, permissionFilters) diff --git a/frontend/src/context/ProjectPermissionContext/index.tsx b/frontend/src/context/ProjectPermissionContext/index.tsx index cf3fe1b15..9eb548c07 100644 --- a/frontend/src/context/ProjectPermissionContext/index.tsx +++ b/frontend/src/context/ProjectPermissionContext/index.tsx @@ -13,6 +13,7 @@ export { ProjectPermissionKmipActions, ProjectPermissionMemberActions, ProjectPermissionPkiSubscriberActions, + ProjectPermissionPkiSyncActions, ProjectPermissionPkiTemplateActions, ProjectPermissionSshHostActions, ProjectPermissionSub diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx index 3bfd02466..81feaf8e5 100644 --- a/frontend/src/context/index.tsx +++ b/frontend/src/context/index.tsx @@ -24,6 +24,7 @@ export { ProjectPermissionKmipActions, ProjectPermissionMemberActions, ProjectPermissionPkiSubscriberActions, + ProjectPermissionPkiSyncActions, ProjectPermissionPkiTemplateActions, ProjectPermissionSshHostActions, ProjectPermissionSub, diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx index e31320d59..b8ba09d3e 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx @@ -72,8 +72,8 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => { data && data.length > 0 && data.map((ca) => { - const canReadCa = permission.can( - ProjectPermissionCertificateAuthorityActions.Read, + const canEditCa = permission.can( + ProjectPermissionCertificateAuthorityActions.Edit, subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name }) @@ -83,12 +83,12 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => { - canReadCa && + canEditCa && handlePopUpOpen("ca", { caId: ca.id, name: ca.name, diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx index e476f429d..2c3b00bc2 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx @@ -6,6 +6,7 @@ import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal } from "@app/components/v2"; import { ProjectPermissionCertificateActions, + ProjectPermissionCertificateProfileActions, ProjectPermissionSub, useProject } from "@app/context"; @@ -104,12 +105,12 @@ export const CertificatesSection = () => {

Certificates

- - {(isAllowed) => ( -
+
+ + {(isAllowed) => ( + )} + + + {(isAllowed) => ( -
- )} - + )} + +
diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx index bc562277d..2be9e35e3 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx @@ -41,8 +41,10 @@ import { import { Badge } from "@app/components/v3"; import { ProjectPermissionCertificateActions, + ProjectPermissionPkiSyncActions, ProjectPermissionSub, - useProject + useProject, + useProjectPermission } from "@app/context"; import { useUpdateRenewalConfig } from "@app/hooks/api"; import { caSupportsCapability } from "@app/hooks/api/ca/constants"; @@ -97,6 +99,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { const [perPage, setPerPage] = useState(PER_PAGE_INIT); const { currentProject } = useProject(); + const { permission } = useProjectPermission(); const { data, isPending } = useListWorkspaceCertificates({ projectId: currentProject?.id ?? "", offset: (page - 1) * perPage, @@ -208,55 +211,72 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { return "opacity-0 group-hover:opacity-100"; })()}`} > - {canShowAutoRenewalIcon && ( - { - if (hasFailed && certificate.renewalError) { - return `Auto-renewal failed: ${certificate.renewalError}`; - } - if (isAutoRenewalEnabled) { - const expiryDate = new Date(certificate.notAfter); - const now = new Date(); - const daysUntilExpiry = Math.ceil( - (expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000) - ); - const daysUntilRenewal = Math.max( - 0, - daysUntilExpiry - (certificate.renewBeforeDays || 0) - ); - return `Auto-renews in ${daysUntilRenewal}d`; - } - return "Set auto renewal"; - })()} - > - - - )} + return ( + { + if (hasFailed && certificate.renewalError) { + return `Auto-renewal failed: ${certificate.renewalError}`; + } + if (isAutoRenewalEnabled) { + const expiryDate = new Date(certificate.notAfter); + const now = new Date(); + const daysUntilExpiry = Math.ceil( + (expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000) + ); + const daysUntilRenewal = Math.max( + 0, + daysUntilExpiry - (certificate.renewBeforeDays || 0) + ); + return `Auto-renews in ${daysUntilRenewal}d`; + } + return "Set auto renewal"; + })()} + > + + + ); + })()}
@@ -481,13 +501,8 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { {certificate.status === CertStatus.ACTIVE && !certificate.renewedByCertificateId && ( {(isAllowed) => ( { )} )} - {/* Only show revoke button if CA supports revocation */} + {/* Only show revoke button if CA supports revocation and certificate is not already revoked */} {(() => { const caType = caCapabilityMap[certificate.caId]; const supportsRevocation = !caType || caSupportsCapability(caType, CaCapability.REVOKE_CERTIFICATES); - if (!supportsRevocation) { + if (!supportsRevocation || isRevoked) { return null; } diff --git a/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx b/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx index 0653d44d0..c734e621e 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx @@ -2,8 +2,15 @@ import { useState } from "react"; import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; +import { PermissionDeniedBanner } from "@app/components/permissions"; import { ContentLoader, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; -import { useProject } from "@app/context"; +import { useProject, useProjectPermission } from "@app/context"; +import { + ProjectPermissionCertificateActions, + ProjectPermissionCertificateProfileActions, + ProjectPermissionPkiTemplateActions, + ProjectPermissionSub +} from "@app/context/ProjectPermissionContext/types"; import { ProjectType } from "@app/hooks/api/projects/types"; import { CertificateProfilesTab } from "./components/CertificateProfilesTab"; @@ -20,8 +27,22 @@ enum TabSections { export const PoliciesPage = () => { const { t } = useTranslation(); const { currentProject } = useProject(); + const { permission } = useProjectPermission(); const [activeTab, setActiveTab] = useState(TabSections.CertificateProfiles); + const canReadCertificateProfiles = permission.can( + ProjectPermissionCertificateProfileActions.Read, + ProjectPermissionSub.CertificateProfiles + ); + const canReadCertificateTemplates = permission.can( + ProjectPermissionPkiTemplateActions.Read, + ProjectPermissionSub.CertificateTemplates + ); + const canReadCertificates = permission.can( + ProjectPermissionCertificateActions.Read, + ProjectPermissionSub.Certificates + ); + if (!currentProject) { return ; } @@ -56,15 +77,19 @@ export const PoliciesPage = () => { - + {canReadCertificateProfiles ? : } - + {canReadCertificateTemplates ? ( + + ) : ( + + )} - + {canReadCertificates ? : } diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx index d55c1b5ef..1da9e8ebe 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx @@ -258,7 +258,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" }; }; - const { control, handleSubmit, reset, watch, setValue, formState } = useForm< + const { control, handleSubmit, reset, watch, setValue, formState, trigger } = useForm< FormData & { preset: TemplatePresetId } >({ resolver: zodResolver(templateSchema), @@ -286,10 +286,11 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" }; const watchedPreset = watch("preset") || TEMPLATE_PRESET_IDS.CUSTOM; - const handlePresetChange = (presetId: TemplatePresetId) => { + const handlePresetChange = async (presetId: TemplatePresetId) => { setValue("preset", presetId); if (presetId === TEMPLATE_PRESET_IDS.CUSTOM) { + await trigger(); return; } @@ -313,6 +314,8 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" if (selectedPreset.formData.keyAlgorithm) { setValue("keyAlgorithm", selectedPreset.formData.keyAlgorithm); } + + await trigger(); } }; From e870b449d6121ab13e3b87b58fef9e8199823593 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Thu, 4 Dec 2025 21:13:43 -0300 Subject: [PATCH 9/9] Skip private key on response if user do not have access to read --- .../certificate-request-service.test.ts | 69 ++++++++++++++++++- .../certificate-request-service.ts | 31 +++++---- .../certificate-v3/certificate-v3-service.ts | 37 +++++++++- 3 files changed, 119 insertions(+), 18 deletions(-) diff --git a/backend/src/services/certificate-request/certificate-request-service.test.ts b/backend/src/services/certificate-request/certificate-request-service.test.ts index 66d5c613b..10c8b73cf 100644 --- a/backend/src/services/certificate-request/certificate-request-service.test.ts +++ b/backend/src/services/certificate-request/certificate-request-service.test.ts @@ -225,6 +225,10 @@ describe("CertificateRequestService", () => { { action: ProjectPermissionCertificateActions.Read, subject: ProjectPermissionSub.Certificates + }, + { + action: ProjectPermissionCertificateActions.ReadPrivateKey, + subject: ProjectPermissionSub.Certificates } ]) }; @@ -319,7 +323,7 @@ describe("CertificateRequestService", () => { }); }); - it("should get certificate from request successfully when private key access is denied", async () => { + it("should get certificate from request successfully when user lacks private key permission", async () => { const mockPermission = { permission: createMongoAbility([ { @@ -349,7 +353,6 @@ describe("CertificateRequestService", () => { (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); (mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithCert); (mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody); - (mockCertificateService.getCertPrivateKey as any).mockRejectedValue(new Error("Private key access denied")); const result = await service.getCertificateFromRequest(mockGetData); @@ -363,8 +366,68 @@ describe("CertificateRequestService", () => { actorAuthMethod: AuthMethod.EMAIL, actorOrgId: "550e8400-e29b-41d4-a716-446655440002" }); + expect(mockCertificateService.getCertPrivateKey).not.toHaveBeenCalled(); + expect(result).toEqual({ + status: CertificateRequestStatus.ISSUED, + certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----", + privateKey: null, + serialNumber: "123456", + errorMessage: null, + createdAt: mockRequestWithCert.createdAt, + updatedAt: mockRequestWithCert.updatedAt + }); + }); + + it("should get certificate from request successfully when user has private key permission but key retrieval fails", async () => { + const mockPermission = { + permission: createMongoAbility([ + { + action: ProjectPermissionCertificateActions.Read, + subject: ProjectPermissionSub.Certificates + }, + { + action: ProjectPermissionCertificateActions.ReadPrivateKey, + subject: ProjectPermissionSub.Certificates + } + ]) + }; + const mockCertificate = { + id: "550e8400-e29b-41d4-a716-446655440009", + serialNumber: "123456", + commonName: "test.example.com" + }; + const mockRequestWithCert = { + id: "550e8400-e29b-41d4-a716-446655440005", + projectId: "550e8400-e29b-41d4-a716-446655440003", + status: CertificateRequestStatus.ISSUED, + certificate: mockCertificate, + errorMessage: null, + createdAt: new Date(), + updatedAt: new Date() + }; + const mockCertBody = { + certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----" + }; + + (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); + (mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithCert); + (mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody); + (mockCertificateService.getCertPrivateKey as any).mockRejectedValue(new Error("Private key not found")); + + const result = await service.getCertificateFromRequest(mockGetData); + + expect(mockCertificateRequestDAL.findByIdWithCertificate).toHaveBeenCalledWith( + "550e8400-e29b-41d4-a716-446655440005" + ); + expect(mockCertificateService.getCertBody).toHaveBeenCalledWith({ + id: "550e8400-e29b-41d4-a716-446655440009", + actor: ActorType.USER, + actorId: "550e8400-e29b-41d4-a716-446655440001", + actorAuthMethod: AuthMethod.EMAIL, + actorOrgId: "550e8400-e29b-41d4-a716-446655440002" + }); expect(mockCertificateService.getCertPrivateKey).toHaveBeenCalledWith({ - id: "550e8400-e29b-41d4-a716-446655440008", + id: "550e8400-e29b-41d4-a716-446655440009", actor: ActorType.USER, actorId: "550e8400-e29b-41d4-a716-446655440001", actorAuthMethod: AuthMethod.EMAIL, diff --git a/backend/src/services/certificate-request/certificate-request-service.ts b/backend/src/services/certificate-request/certificate-request-service.ts index 71129aba7..46cd49476 100644 --- a/backend/src/services/certificate-request/certificate-request-service.ts +++ b/backend/src/services/certificate-request/certificate-request-service.ts @@ -218,20 +218,25 @@ export const certificateRequestServiceFactory = ({ actorOrgId }); - // Try to get private key (may fail if user doesn't have permission) + const canReadPrivateKey = permission.can( + ProjectPermissionCertificateActions.ReadPrivateKey, + ProjectPermissionSub.Certificates + ); + let privateKey: string | null = null; - try { - const certPrivateKey = await certificateService.getCertPrivateKey({ - id: certificateRequest.certificate.id, - actor, - actorId, - actorAuthMethod, - actorOrgId - }); - privateKey = certPrivateKey.certPrivateKey; - } catch (error) { - // Private key access denied - continue without it - privateKey = null; + if (canReadPrivateKey) { + try { + const certPrivateKey = await certificateService.getCertPrivateKey({ + id: certificateRequest.certificate.id, + actor, + actorId, + actorAuthMethod, + actorOrgId + }); + privateKey = certPrivateKey.certPrivateKey; + } catch (error) { + privateKey = null; + } } return { diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index af6162d70..c20412537 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -1001,11 +1001,27 @@ export const certificateV3ServiceFactory = ({ }); } + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: profile.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + const canReadPrivateKey = permission.can( + ProjectPermissionCertificateActions.ReadPrivateKey, + ProjectPermissionSub.Certificates + ); + + const privateKeyForResponse = canReadPrivateKey ? selfSignedResult.privateKey.toString("utf8") : undefined; + return { certificate: selfSignedResult.certificate.toString("utf8"), issuingCaCertificate: "", certificateChain: selfSignedResult.certificate.toString("utf8"), - privateKey: selfSignedResult.privateKey.toString("utf8"), + privateKey: privateKeyForResponse, serialNumber: selfSignedResult.serialNumber, certificateId: certificateData.id, certificateRequestId, @@ -1102,11 +1118,28 @@ export const certificateV3ServiceFactory = ({ finalCertificateChain = removeRootCaFromChain(finalCertificateChain); } + // Check if user has permission to read private key + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: profile.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + const canReadPrivateKey = permission.can( + ProjectPermissionCertificateActions.ReadPrivateKey, + ProjectPermissionSub.Certificates + ); + + const privateKeyForResponse = canReadPrivateKey ? bufferToString(privateKey) : undefined; + return { certificate: bufferToString(certificate), issuingCaCertificate: bufferToString(issuingCaCertificate), certificateChain: finalCertificateChain, - privateKey: bufferToString(privateKey), + privateKey: privateKeyForResponse, serialNumber, certificateId: cert.id, certificateRequestId,