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 ; }