diff --git a/.infisicalignore b/.infisicalignore index d0f302ce0..441031b33 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -57,4 +57,5 @@ docs/documentation/platform/pki/enrollment-methods/api.mdx:generic-api-key:93 docs/documentation/platform/pki/enrollment-methods/api.mdx:private-key:139 docs/documentation/platform/pki/certificate-syncs/aws-secrets-manager.mdx:private-key:62 docs/documentation/platform/pki/certificate-syncs/chef.mdx:private-key:61 -backend/src/services/certificate-request/certificate-request-service.test.ts:private-key:246 \ No newline at end of file +backend/src/services/certificate-request/certificate-request-service.test.ts:private-key:246 +backend/src/services/certificate-request/certificate-request-service.test.ts:private-key:248 \ No newline at end of file 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 8c45ed8f8..7b2e0ae00 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, @@ -45,7 +46,6 @@ const buildAdminPermissionRules = () => { ProjectPermissionSub.Environments, ProjectPermissionSub.Tags, ProjectPermissionSub.IpAllowList, - ProjectPermissionSub.CertificateAuthorities, ProjectPermissionSub.PkiAlerts, ProjectPermissionSub.PkiCollections, ProjectPermissionSub.SshCertificateAuthorities, @@ -68,6 +68,18 @@ const buildAdminPermissionRules = () => { can([ProjectPermissionAuditLogsActions.Read], ProjectPermissionSub.AuditLogs); + can( + [ + ProjectPermissionCertificateAuthorityActions.Read, + ProjectPermissionCertificateAuthorityActions.Create, + ProjectPermissionCertificateAuthorityActions.Edit, + ProjectPermissionCertificateAuthorityActions.Delete, + ProjectPermissionCertificateAuthorityActions.Renew, + ProjectPermissionCertificateAuthorityActions.SignIntermediate + ], + ProjectPermissionSub.CertificateAuthorities + ); + can( [ ProjectPermissionPkiTemplateActions.Read, @@ -96,7 +108,8 @@ const buildAdminPermissionRules = () => { ProjectPermissionCertificateActions.Edit, ProjectPermissionCertificateActions.Create, ProjectPermissionCertificateActions.Delete, - ProjectPermissionCertificateActions.ReadPrivateKey + ProjectPermissionCertificateActions.ReadPrivateKey, + ProjectPermissionCertificateActions.Import ], ProjectPermissionSub.Certificates ); @@ -461,7 +474,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( @@ -469,7 +482,8 @@ const buildMemberPermissionRules = () => { ProjectPermissionCertificateActions.Read, ProjectPermissionCertificateActions.Edit, ProjectPermissionCertificateActions.Create, - ProjectPermissionCertificateActions.Delete + ProjectPermissionCertificateActions.Delete, + ProjectPermissionCertificateActions.Import ], ProjectPermissionSub.Certificates ); @@ -600,7 +614,7 @@ 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(ProjectPermissionPkiTemplateActions.Read, ProjectPermissionSub.CertificateTemplates); can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 19340644a..20f9c1f09 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -23,12 +23,22 @@ export enum ProjectPermissionCommitsActions { PerformRollback = "perform-rollback" } +export enum ProjectPermissionCertificateAuthorityActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + Renew = "renew", + SignIntermediate = "sign-intermediate" +} + export enum ProjectPermissionCertificateActions { Read = "read", Create = "create", Edit = "edit", Delete = "delete", - ReadPrivateKey = "read-private-key" + ReadPrivateKey = "read-private-key", + Import = "import" } export enum ProjectPermissionSecretActions { @@ -292,7 +302,8 @@ export type SecretSyncSubjectFields = { }; export type PkiSyncSubjectFields = { - subscriberName: string; + subscriberName?: string; + name: string; }; export type DynamicSecretSubjectFields = { @@ -332,6 +343,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 +430,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, ( @@ -454,7 +494,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,6 +618,17 @@ const SecretSyncConditionV2Schema = z const PkiSyncConditionSchema = 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() + ]), subscriberName: z.union([ z.string(), z @@ -698,6 +755,7 @@ const PkiTemplateConditionSchema = z z .object({ [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB], [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] }) @@ -749,6 +807,98 @@ 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], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] + }) + .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 GeneralPermissionSchema = [ z.object({ subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), @@ -828,18 +978,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 +1268,30 @@ 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() }), ...GeneralPermissionSchema ]); diff --git a/backend/src/lib/casl/permission-filter-utils.ts b/backend/src/lib/casl/permission-filter-utils.ts new file mode 100644 index 000000000..35f519c33 --- /dev/null +++ b/backend/src/lib/casl/permission-filter-utils.ts @@ -0,0 +1,225 @@ +import type { MongoAbility, MongoQuery, RawRuleOf } from "@casl/ability"; +import RE2 from "re2"; + +export interface PermissionFilterConfig { + operator: string; + value: unknown; + isPattern: boolean; + isInverted?: boolean; +} + +export type PermissionFilters = Record>; + +export interface ProcessedPermissionRules { + allowRules: Array>>; + forbidRules: Array>>; +} + +interface MongoRegexFilter { + $regex: RegExp; +} + +interface MongoEqFilter { + $eq: unknown; +} + +interface MongoInFilter { + $in: unknown[]; +} + +interface MongoNeFilter { + $ne: unknown; +} + +interface MongoGlobFilter { + $glob: unknown; +} + +/** + * Builds permission filters from CASL MongoDB-style conditions + * @param conditions - MongoDB-style conditions from CASL ability + * @param isInverted - Whether this rule is inverted (forbidden) + * @returns Record of field names to arrays of filter configurations + */ +const buildPermissionFiltersFromConditions = (conditions: MongoQuery, isInverted = false): PermissionFilters => { + const permissionFilters: PermissionFilters = {}; + + function addFilterToField(key: string, operator: string, value: unknown, isPattern: boolean) { + if (!permissionFilters[key]) { + permissionFilters[key] = []; + } + + // Convert operators for inverted/forbidden rules + let finalOperator = operator; + if (isInverted) { + switch (operator) { + case "=": + finalOperator = "!="; + break; + case "!=": + finalOperator = "="; + break; + case "LIKE": + finalOperator = "NOT LIKE"; + break; + case "NOT LIKE": + finalOperator = "LIKE"; + break; + case "IN": + finalOperator = "NOT IN"; + break; + case "NOT IN": + finalOperator = "IN"; + break; + case ">": + finalOperator = "<="; + break; + case ">=": + finalOperator = "<"; + break; + case "<": + finalOperator = ">="; + break; + case "<=": + finalOperator = ">"; + break; + case "IS NULL": + finalOperator = "IS NOT NULL"; + break; + case "IS NOT NULL": + finalOperator = "IS NULL"; + break; + // Default: keep the same operator + default: + finalOperator = operator; + break; + } + } + + permissionFilters[key].push({ operator: finalOperator, value, isPattern, isInverted }); + } + + function processCondition(key: string, value: unknown) { + if (value && typeof value === "object") { + const valueObj = value as Record; + + const operatorKeys = ["$regex", "$eq", "$in", "$glob", "$ne"]; + const presentOperators = operatorKeys.filter((op) => op in valueObj); + + if (presentOperators.length > 1) { + if ("$eq" in valueObj) { + addFilterToField(key, "=", valueObj.$eq, false); + } + if ("$glob" in valueObj) { + addFilterToField(key, "LIKE", valueObj.$glob, true); + } + if ("$regex" in valueObj) { + const regexValue = valueObj.$regex as RegExp; + const regexPattern = regexValue.source; + const globPattern = regexPattern + .replace(new RE2("^\\\\\\^"), "") + .replace(new RE2("\\\\\\$$"), "") + .replace(new RE2("\\\\\\.\\*", "g"), "*"); + addFilterToField(key, "LIKE", globPattern, true); + } + if ("$ne" in valueObj) { + const valueStr = String(valueObj.$ne); + const hasWildcards = valueStr.includes("*") || valueStr.includes("?"); + addFilterToField(key, hasWildcards ? "NOT LIKE" : "!=", valueObj.$ne, hasWildcards); + } + if ("$in" in valueObj) { + const inValues = valueObj.$in as unknown[]; + addFilterToField(key, "IN", inValues, false); + } + } else if ("$regex" in value) { + const regexFilter = value as MongoRegexFilter; + const regexPattern = regexFilter.$regex.source; + const globPattern = regexPattern + .replace(new RE2("^\\\\\\^"), "") + .replace(new RE2("\\\\\\$$"), "") + .replace(new RE2("\\\\\\.\\*", "g"), "*"); + addFilterToField(key, "LIKE", globPattern, true); + } else if ("$eq" in value) { + const eqFilter = value as MongoEqFilter; + addFilterToField(key, "=", eqFilter.$eq, false); + } else if ("$in" in value) { + const inFilter = value as MongoInFilter; + addFilterToField(key, "IN", inFilter.$in, false); + } else if ("$glob" in value) { + const globFilter = value as MongoGlobFilter; + addFilterToField(key, "LIKE", globFilter.$glob, true); + } else if ("$ne" in value) { + const neFilter = value as MongoNeFilter; + const valueStr = String(neFilter.$ne); + const hasWildcards = valueStr.includes("*") || valueStr.includes("?"); + addFilterToField(key, hasWildcards ? "NOT LIKE" : "!=", neFilter.$ne, hasWildcards); + } + } else { + addFilterToField(key, "=", value, false); + } + } + + function processConditions(mongoConditions: MongoQuery) { + if ( + mongoConditions && + typeof mongoConditions === "object" && + "$or" in mongoConditions && + Array.isArray(mongoConditions.$or) + ) { + mongoConditions.$or.forEach((orCondition: MongoQuery) => { + processConditions(orCondition); + }); + } else if (mongoConditions && typeof mongoConditions === "object") { + Object.entries(mongoConditions).forEach(([key, value]) => { + if (key.startsWith("$")) return; + processCondition(key, value); + }); + } + } + + if (conditions && typeof conditions === "object") { + processConditions(conditions); + } + + return permissionFilters; +}; + +/** + * Extract permission filters for a subject and action, + * converting them into ProcessedPermissionRules format for use with Knex queries. + * @param ability - CASL MongoAbility instance + * @param action - Permission action to filter for + * @param subjectName - Permission subject to filter for + * @returns ProcessedPermissionRules object for use with applyPermissionFiltersToQuery + */ +export function getProcessedPermissionRules( + ability: MongoAbility, + action: string, + subjectName: string +): ProcessedPermissionRules { + const matchingRules = ability.rules.filter((rule: RawRuleOf) => { + const actionMatches = Array.isArray(rule.action) ? rule.action.includes(action) : rule.action === action; + const subjectMatches = Array.isArray(rule.subject) + ? rule.subject.includes(subjectName) + : rule.subject === subjectName; + return actionMatches && subjectMatches && rule.conditions; + }); + + const allowRules: Array>> = []; + const forbidRules: Array>> = []; + + matchingRules.forEach((rule: RawRuleOf) => { + if (rule.conditions) { + const isInverted = rule.inverted || false; + const ruleFilters = buildPermissionFiltersFromConditions(rule.conditions, isInverted); + + if (isInverted) { + forbidRules.push(ruleFilters); + } else { + allowRules.push(ruleFilters); + } + } + }); + + return { allowRules, forbidRules }; +} diff --git a/backend/src/lib/knex/permission-filter-utils.ts b/backend/src/lib/knex/permission-filter-utils.ts new file mode 100644 index 000000000..d6e0f92a4 --- /dev/null +++ b/backend/src/lib/knex/permission-filter-utils.ts @@ -0,0 +1,145 @@ +import type { Knex } from "knex"; +import RE2 from "re2"; + +export interface PermissionFilterConfig { + operator: string; + value: unknown; + isPattern: boolean; + isInverted?: boolean; +} + +export type PermissionFilters = Record>; + +export interface ProcessedPermissionRules { + allowRules: Array>>; + forbidRules: Array>>; +} + +/** + * Applies a single filter configuration to a query + * @param query - The Knex query builder instance + * @param tableName - The name of the table to apply filters to + * @param key - The field name + * @param filterConfig - The filter configuration + */ +const applySingleFilter = ( + query: Knex.QueryBuilder, + tableName: string, + key: string, + filterConfig: PermissionFilterConfig +): void => { + if (filterConfig.value !== undefined && filterConfig.value !== null) { + const { operator, value, isPattern } = filterConfig; + const fieldName = `${tableName}.${key}`; + + switch (operator) { + case "=": + void query.andWhere(fieldName, "=", value as string | number); + break; + case "!=": + void query.andWhere(fieldName, "!=", value as string | number); + break; + case "LIKE": { + const likePattern = isPattern ? String(value).replace(new RE2("\\*", "g"), "%") : String(value); + void query.andWhere(fieldName, "like", likePattern); + break; + } + case "NOT LIKE": { + const notLikePattern = isPattern ? String(value).replace(new RE2("\\*", "g"), "%") : String(value); + void query.andWhere(fieldName, "not like", notLikePattern); + break; + } + case "IN": { + const inValues = Array.isArray(value) ? value : [value]; + void query.andWhere(fieldName, "in", inValues as (string | number)[]); + break; + } + case "NOT IN": { + const notInValues = Array.isArray(value) ? value : [value]; + void query.andWhere(fieldName, "not in", notInValues as (string | number)[]); + break; + } + case ">": + void query.andWhere(fieldName, ">", value as string | number); + break; + case ">=": + void query.andWhere(fieldName, ">=", value as string | number); + break; + case "<": + void query.andWhere(fieldName, "<", value as string | number); + break; + case "<=": + void query.andWhere(fieldName, "<=", value as string | number); + break; + case "IS NULL": + void query.andWhere(fieldName, "is", null); + break; + case "IS NOT NULL": + void query.andWhere(fieldName, "is not", null); + break; + default: + void query.andWhere(fieldName, "=", value as string | number); + break; + } + } +}; + +/** + * Applies complex permission rules to a Knex query with proper OR/AND logic + * @param query - The Knex query builder instance + * @param tableName - The name of the table to apply filters to + * @param processedRules - Processed permission rules with allow and forbid rules + * @returns The modified query builder with permission rules applied + */ +export const applyProcessedPermissionRulesToQuery = ( + originalQuery: Knex.QueryBuilder, + tableName: string, + processedRules?: ProcessedPermissionRules +): Knex.QueryBuilder => { + if (!processedRules || (processedRules.allowRules.length === 0 && processedRules.forbidRules.length === 0)) { + return originalQuery; + } + + let query = originalQuery; + + if (processedRules.allowRules.length > 0) { + query = query.andWhere((allowBuilder) => { + processedRules.allowRules.forEach((rule, index) => { + const ruleBuilder = (ruleSubBuilder: Knex.QueryBuilder) => { + Object.entries(rule).forEach(([key, filterConfigs]) => { + filterConfigs.forEach((filterConfig) => { + applySingleFilter(ruleSubBuilder, tableName, key, filterConfig); + }); + }); + }; + + if (index === 0) { + void allowBuilder.where(ruleBuilder); + } else { + void allowBuilder.orWhere(ruleBuilder); + } + }); + }); + } + + if (processedRules.forbidRules.length > 0) { + processedRules.forbidRules.forEach((forbidRule) => { + Object.entries(forbidRule).forEach(([key, filterConfigs]) => { + filterConfigs.forEach((filterConfig) => { + applySingleFilter(query, tableName, key, filterConfig); + }); + }); + }); + } + + return query; +}; + +/** + * Sanitizes a string value for safe use in SQL LIKE queries + * @param value - The string value to sanitize + * @returns The sanitized string with SQL special characters escaped + */ +export const sanitizeForLike = (value: string): string => { + return String(value).replace(new RE2("[%_\\\\]", "g"), "\\$&"); +}; diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index f14ff437f..397085171 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -176,7 +176,8 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId + actorOrgId: req.permission.orgId, + isInternal: true }); const caType = (ca?.externalCa?.type as CaType) ?? CaType.INTERNAL; useOrderFlow = caType !== CaType.INTERNAL; diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts index b48b1076f..9616b3090 100644 --- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts @@ -7,6 +7,7 @@ import { TableName } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, CryptographyError, NotFoundError } from "@app/lib/errors"; +import { ProcessedPermissionRules } from "@app/lib/knex/permission-filter-utils"; import { OrgServiceActor } from "@app/lib/types"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; @@ -715,11 +716,21 @@ export const AcmeCertificateAuthorityFns = ({ return castDbEntryToAcmeCertificateAuthority(updatedCa); }; - const listCertificateAuthorities = async ({ projectId }: { projectId: string }) => { - const cas = await certificateAuthorityDAL.findWithAssociatedCa({ - [`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, - [`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.ACME - }); + const listCertificateAuthorities = async ({ + projectId, + permissionFilters + }: { + projectId: string; + permissionFilters?: ProcessedPermissionRules; + }) => { + const cas = await certificateAuthorityDAL.findWithAssociatedCa( + { + [`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, + [`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.ACME + }, + {}, + permissionFilters + ); return cas.map(castDbEntryToAcmeCertificateAuthority); }; diff --git a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts index 421da28aa..7ebd66146 100644 --- a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts @@ -5,6 +5,7 @@ import RE2 from "re2"; import { TableName } from "@app/db/schemas"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { ProcessedPermissionRules } from "@app/lib/knex/permission-filter-utils"; import { ms } from "@app/lib/ms"; import { OrgServiceActor } from "@app/lib/types"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; @@ -798,11 +799,21 @@ export const AzureAdCsCertificateAuthorityFns = ({ return castDbEntryToAzureAdCsCertificateAuthority(updatedCa); }; - const listCertificateAuthorities = async ({ projectId }: { projectId: string }) => { - const cas = await certificateAuthorityDAL.findWithAssociatedCa({ - [`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, - [`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.AZURE_AD_CS - }); + const listCertificateAuthorities = async ({ + projectId, + permissionFilters + }: { + projectId: string; + permissionFilters?: ProcessedPermissionRules; + }) => { + const cas = await certificateAuthorityDAL.findWithAssociatedCa( + { + [`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, + [`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.AZURE_AD_CS + }, + {}, + permissionFilters + ); return cas.map(castDbEntryToAzureAdCsCertificateAuthority); }; diff --git a/backend/src/services/certificate-authority/certificate-authority-dal.ts b/backend/src/services/certificate-authority/certificate-authority-dal.ts index 352675441..6f621a515 100644 --- a/backend/src/services/certificate-authority/certificate-authority-dal.ts +++ b/backend/src/services/certificate-authority/certificate-authority-dal.ts @@ -4,6 +4,10 @@ import { TDbClient } from "@app/db"; import { CertificateAuthoritiesSchema, TableName, TCertificateAuthorities } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { buildFindFilter, ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex"; +import { + applyProcessedPermissionRulesToQuery, + type ProcessedPermissionRules +} from "@app/lib/knex/permission-filter-utils"; export type TCertificateAuthorityDALFactory = ReturnType; @@ -220,10 +224,11 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { const findWithAssociatedCa = async ( filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string; serialNumber?: string }, { offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt = {}, + permissionFilters?: ProcessedPermissionRules, tx?: Knex ) => { try { - const query = (tx || db.replicaNode())(TableName.CertificateAuthority) + let query = (tx || db.replicaNode())(TableName.CertificateAuthority) .leftJoin( TableName.InternalCertificateAuthority, `${TableName.CertificateAuthority}.id`, @@ -268,6 +273,14 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { db.ref("appConnectionId").withSchema(TableName.ExternalCertificateAuthority).as("externalAppConnectionId") ); + if (permissionFilters) { + query = applyProcessedPermissionRulesToQuery( + query, + TableName.CertificateAuthority, + permissionFilters + ) as typeof query; + } + if (limit) void query.limit(limit); if (offset) void query.offset(offset); if (sort) { diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index be53f08ce..a6dd4ce9e 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -1,8 +1,12 @@ -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 { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; @@ -142,8 +146,8 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Create, + subject(ProjectPermissionSub.CertificateAuthorities, { name }) ); if (type === CaType.INTERNAL) { @@ -212,8 +216,8 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Read, + subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name }) ); if (type === CaType.INTERNAL) { @@ -227,6 +231,7 @@ export const certificateAuthorityServiceFactory = ({ id: certificateAuthority.id, type, enableDirectIssuance: certificateAuthority.enableDirectIssuance, + subject: ProjectPermissionSub.CertificateAuthorities, name: certificateAuthority.name, projectId: certificateAuthority.projectId, configuration: certificateAuthority.internalCa, @@ -275,8 +280,8 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Read, + subject(ProjectPermissionSub.CertificateAuthorities, { name: caName }) ); if (type === CaType.INTERNAL) { @@ -328,15 +333,25 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionCertificateAuthorityActions.Read, + ProjectPermissionSub.CertificateAuthorities + ); + + const permissionFilters = getProcessedPermissionRules( + permission, + ProjectPermissionCertificateAuthorityActions.Read, ProjectPermissionSub.CertificateAuthorities ); if (type === CaType.INTERNAL) { - const cas = await certificateAuthorityDAL.findWithAssociatedCa({ - [`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, - $notNull: [`${TableName.InternalCertificateAuthority}.id` as "id"] - }); + const cas = await certificateAuthorityDAL.findWithAssociatedCa( + { + [`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, + $notNull: [`${TableName.InternalCertificateAuthority}.id` as "id"] + }, + {}, + permissionFilters + ); return cas .filter((ca): ca is typeof ca & { internalCa: NonNullable } => Boolean(ca.internalCa)) @@ -352,11 +367,11 @@ export const certificateAuthorityServiceFactory = ({ } if (type === CaType.ACME) { - return acmeFns.listCertificateAuthorities({ projectId }); + return acmeFns.listCertificateAuthorities({ projectId, permissionFilters }); } if (type === CaType.AZURE_AD_CS) { - return azureAdCsFns.listCertificateAuthorities({ projectId }); + return azureAdCsFns.listCertificateAuthorities({ projectId, permissionFilters }); } throw new BadRequestError({ message: "Invalid certificate authority type" }); @@ -383,8 +398,8 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Edit, + subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name }) ); if (type === CaType.INTERNAL) { @@ -459,8 +474,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) { @@ -524,8 +539,8 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Edit, + subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name }) ); if (type === CaType.INTERNAL) { @@ -606,8 +621,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) { @@ -662,6 +677,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, @@ -672,8 +694,10 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Read, + subject(ProjectPermissionSub.CertificateAuthorities, { + name: certificateAuthority.name + }) ); return azureAdCsFns.getTemplates({ @@ -687,32 +711,38 @@ export const certificateAuthorityServiceFactory = ({ actor, actorId, actorAuthMethod, - actorOrgId + actorOrgId, + isInternal }: { caId: string; actor: OrgServiceActor["type"]; actorId: string; actorAuthMethod: OrgServiceActor["authMethod"]; actorOrgId?: string; + isInternal?: boolean; }) => { const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); if (!ca) { throw new NotFoundError({ message: "CA not found" }); } - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: ca.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); + if (!isInternal) { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateAuthorities - ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateAuthorityActions.Read, + subject(ProjectPermissionSub.CertificateAuthorities, { + name: ca.name + }) + ); + } return ca; }; 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 2b7c155db..91b7727ec 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 @@ -174,8 +174,8 @@ export const internalCertificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Create, + subject(ProjectPermissionSub.CertificateAuthorities, { name: commonName }) ); } else { projectId = dto.projectId; @@ -357,8 +357,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); @@ -383,8 +383,8 @@ export const internalCertificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Edit, + subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name }) ); } @@ -416,8 +416,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); @@ -442,8 +442,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) @@ -506,8 +506,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" }); @@ -793,8 +793,8 @@ export const internalCertificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - ProjectPermissionSub.CertificateAuthorities + ProjectPermissionCertificateAuthorityActions.Read, + subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name }) ); const caCertChains = await getCaCertChains({ @@ -830,8 +830,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({ @@ -911,8 +911,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" }); @@ -1059,8 +1059,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-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index 7a6494d34..3a8f99f1a 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -4,6 +4,10 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { + applyProcessedPermissionRulesToQuery, + type ProcessedPermissionRules +} from "@app/lib/knex/permission-filter-utils"; import { EnrollmentType, @@ -276,6 +280,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { issuerType?: IssuerType; caId?: string; } = {}, + processedRules?: ProcessedPermissionRules, tx?: Knex ): Promise => { try { @@ -308,7 +313,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.issuerType`, issuerType); } - const query = baseQuery + let query = baseQuery .leftJoin( TableName.CertificateAuthority, `${TableName.PkiCertificateProfile}.caId`, @@ -354,6 +359,14 @@ export const certificateProfileDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeId") ); + if (processedRules) { + query = applyProcessedPermissionRulesToQuery( + query, + TableName.PkiCertificateProfile, + processedRules + ) as typeof query; + } + const results = (await query .orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc") .offset(offset) @@ -432,6 +445,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { issuerType?: IssuerType; caId?: string; } = {}, + processedRules?: ProcessedPermissionRules, tx?: Knex ): Promise => { try { @@ -459,6 +473,14 @@ export const certificateProfileDALFactory = (db: TDbClient) => { query = query.where({ issuerType }); } + if (processedRules) { + query = applyProcessedPermissionRulesToQuery( + query, + TableName.PkiCertificateProfile, + processedRules + ) as typeof query; + } + const result = await query.count("*").first(); return parseInt((result as unknown as { count: string }).count || "0", 10); } catch (error) { diff --git a/backend/src/services/certificate-profile/certificate-profile-service.test.ts b/backend/src/services/certificate-profile/certificate-profile-service.test.ts index 17ddcaa30..d10188e6f 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.test.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.test.ts @@ -169,7 +169,8 @@ describe("CertificateProfileService", () => { const mockPermissionService = { getProjectPermission: vi.fn().mockResolvedValue({ permission: { - throwUnlessCan: vi.fn() + throwUnlessCan: vi.fn(), + rules: [] } }) } as unknown as Pick; @@ -598,13 +599,18 @@ describe("CertificateProfileService", () => { expect(result.profiles).toEqual(mockProfiles); expect(result.totalCount).toBe(1); - expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith("project-123", { - offset: 0, - limit: 20, - search: undefined, - enrollmentType: undefined, - caId: undefined - }); + expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith( + "project-123", + { + offset: 0, + limit: 20, + search: undefined, + enrollmentType: undefined, + caId: undefined, + issuerType: undefined + }, + { allowRules: [], forbidRules: [] } + ); }); it("should list profiles with filters", async () => { @@ -618,13 +624,18 @@ describe("CertificateProfileService", () => { caId: "ca-123" }); - expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith("project-123", { - offset: 10, - limit: 5, - search: "test", - enrollmentType: EnrollmentType.API, - caId: "ca-123" - }); + expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith( + "project-123", + { + offset: 10, + limit: 5, + search: "test", + enrollmentType: EnrollmentType.API, + caId: "ca-123", + issuerType: undefined + }, + { allowRules: [], forbidRules: [] } + ); }); }); diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index a771b7b0a..59e3afbd9 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"; @@ -10,6 +10,7 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { buildUrl } from "@app/ee/services/pki-acme/pki-acme-fns"; +import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils"; import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; @@ -299,7 +300,9 @@ export const certificateProfileServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.Create, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug: data.slug + }) ); const project = await projectDAL.findById(projectId); @@ -454,7 +457,9 @@ export const certificateProfileServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.Edit, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug: existingProfile.slug + }) ); if (data.certificateTemplateId) { @@ -577,7 +582,9 @@ export const certificateProfileServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.Read, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug: profile.slug + }) ); const converted = convertDalToService(profile); @@ -613,7 +620,9 @@ export const certificateProfileServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.Read, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug: profile.slug + }) ); if (profile.estConfig && profile.estConfig.caChain) { @@ -687,7 +696,9 @@ export const certificateProfileServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.Read, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug + }) ); const profile = await certificateProfileDAL.findBySlugAndProjectId(slug, projectId); @@ -739,21 +750,35 @@ export const certificateProfileServiceFactory = ({ ProjectPermissionSub.CertificateProfiles ); - const profiles = await certificateProfileDAL.findByProjectId(projectId, { - offset, - limit, - search, - enrollmentType, - issuerType, - caId - }); + const processedRules = getProcessedPermissionRules( + permission, + ProjectPermissionCertificateProfileActions.Read, + ProjectPermissionSub.CertificateProfiles + ); - const totalCount = await certificateProfileDAL.countByProjectId(projectId, { - search, - enrollmentType, - issuerType, - caId - }); + const profiles = await certificateProfileDAL.findByProjectId( + projectId, + { + offset, + limit, + search, + enrollmentType, + issuerType, + caId + }, + processedRules + ); + + const totalCount = await certificateProfileDAL.countByProjectId( + projectId, + { + search, + enrollmentType, + issuerType, + caId + }, + processedRules + ); const convertedProfiles = await Promise.all( profiles.map(async (profile) => { @@ -838,7 +863,9 @@ export const certificateProfileServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.Delete, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug: profile.slug + }) ); const deletedProfile = await certificateProfileDAL.deleteById(profileId); @@ -884,7 +911,9 @@ export const certificateProfileServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.Read, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug: profile.slug + }) ); const certificates = await certificateProfileDAL.getCertificatesByProfile(profileId, { @@ -925,17 +954,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); @@ -944,6 +965,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({ @@ -1037,7 +1076,9 @@ export const certificateProfileServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.Read, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug: profile.slug + }) ); } @@ -1088,7 +1129,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-request/certificate-request-service.test.ts b/backend/src/services/certificate-request/certificate-request-service.test.ts index 5e6b870bf..10c8b73cf 100644 --- a/backend/src/services/certificate-request/certificate-request-service.test.ts +++ b/backend/src/services/certificate-request/certificate-request-service.test.ts @@ -10,6 +10,7 @@ import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionCertificateActions, + ProjectPermissionCertificateProfileActions, ProjectPermissionSet, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -79,8 +80,8 @@ describe("CertificateRequestService", () => { const mockPermission = { permission: createMongoAbility([ { - action: ProjectPermissionCertificateActions.Create, - subject: ProjectPermissionSub.Certificates + action: ProjectPermissionCertificateProfileActions.IssueCert, + subject: ProjectPermissionSub.CertificateProfiles } ]) }; @@ -224,6 +225,10 @@ describe("CertificateRequestService", () => { { action: ProjectPermissionCertificateActions.Read, subject: ProjectPermissionSub.Certificates + }, + { + action: ProjectPermissionCertificateActions.ReadPrivateKey, + subject: ProjectPermissionSub.Certificates } ]) }; @@ -318,7 +323,7 @@ describe("CertificateRequestService", () => { }); }); - it("should get certificate from request successfully when private key access is denied", async () => { + it("should get certificate from request successfully when user lacks private key permission", async () => { const mockPermission = { permission: createMongoAbility([ { @@ -348,7 +353,6 @@ describe("CertificateRequestService", () => { (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); (mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithCert); (mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody); - (mockCertificateService.getCertPrivateKey as any).mockRejectedValue(new Error("Private key access denied")); const result = await service.getCertificateFromRequest(mockGetData); @@ -362,8 +366,68 @@ describe("CertificateRequestService", () => { actorAuthMethod: AuthMethod.EMAIL, actorOrgId: "550e8400-e29b-41d4-a716-446655440002" }); + expect(mockCertificateService.getCertPrivateKey).not.toHaveBeenCalled(); + expect(result).toEqual({ + status: CertificateRequestStatus.ISSUED, + certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----", + privateKey: null, + serialNumber: "123456", + errorMessage: null, + createdAt: mockRequestWithCert.createdAt, + updatedAt: mockRequestWithCert.updatedAt + }); + }); + + it("should get certificate from request successfully when user has private key permission but key retrieval fails", async () => { + const mockPermission = { + permission: createMongoAbility([ + { + action: ProjectPermissionCertificateActions.Read, + subject: ProjectPermissionSub.Certificates + }, + { + action: ProjectPermissionCertificateActions.ReadPrivateKey, + subject: ProjectPermissionSub.Certificates + } + ]) + }; + const mockCertificate = { + id: "550e8400-e29b-41d4-a716-446655440009", + serialNumber: "123456", + commonName: "test.example.com" + }; + const mockRequestWithCert = { + id: "550e8400-e29b-41d4-a716-446655440005", + projectId: "550e8400-e29b-41d4-a716-446655440003", + status: CertificateRequestStatus.ISSUED, + certificate: mockCertificate, + errorMessage: null, + createdAt: new Date(), + updatedAt: new Date() + }; + const mockCertBody = { + certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----" + }; + + (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); + (mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithCert); + (mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody); + (mockCertificateService.getCertPrivateKey as any).mockRejectedValue(new Error("Private key not found")); + + const result = await service.getCertificateFromRequest(mockGetData); + + expect(mockCertificateRequestDAL.findByIdWithCertificate).toHaveBeenCalledWith( + "550e8400-e29b-41d4-a716-446655440005" + ); + expect(mockCertificateService.getCertBody).toHaveBeenCalledWith({ + id: "550e8400-e29b-41d4-a716-446655440009", + actor: ActorType.USER, + actorId: "550e8400-e29b-41d4-a716-446655440001", + actorAuthMethod: AuthMethod.EMAIL, + actorOrgId: "550e8400-e29b-41d4-a716-446655440002" + }); expect(mockCertificateService.getCertPrivateKey).toHaveBeenCalledWith({ - id: "550e8400-e29b-41d4-a716-446655440008", + id: "550e8400-e29b-41d4-a716-446655440009", actor: ActorType.USER, actorId: "550e8400-e29b-41d4-a716-446655440001", actorAuthMethod: AuthMethod.EMAIL, diff --git a/backend/src/services/certificate-request/certificate-request-service.ts b/backend/src/services/certificate-request/certificate-request-service.ts index 15450ee6e..46cd49476 100644 --- a/backend/src/services/certificate-request/certificate-request-service.ts +++ b/backend/src/services/certificate-request/certificate-request-service.ts @@ -6,6 +6,7 @@ import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionCertificateActions, + ProjectPermissionCertificateProfileActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -110,8 +111,8 @@ export const certificateRequestServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateActions.Create, - ProjectPermissionSub.Certificates + ProjectPermissionCertificateProfileActions.IssueCert, + ProjectPermissionSub.CertificateProfiles ); } @@ -217,20 +218,25 @@ export const certificateRequestServiceFactory = ({ actorOrgId }); - // Try to get private key (may fail if user doesn't have permission) + const canReadPrivateKey = permission.can( + ProjectPermissionCertificateActions.ReadPrivateKey, + ProjectPermissionSub.Certificates + ); + let privateKey: string | null = null; - try { - const certPrivateKey = await certificateService.getCertPrivateKey({ - id: certificateRequest.certificate.id, - actor, - actorId, - actorAuthMethod, - actorOrgId - }); - privateKey = certPrivateKey.certPrivateKey; - } catch (error) { - // Private key access denied - continue without it - privateKey = null; + if (canReadPrivateKey) { + try { + const certPrivateKey = await certificateService.getCertPrivateKey({ + id: certificateRequest.certificate.id, + actor, + actorId, + actorAuthMethod, + actorOrgId + }); + privateKey = certPrivateKey.certPrivateKey; + } catch (error) { + privateKey = null; + } } return { diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts index 3b935f26a..4951986b1 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts @@ -5,6 +5,10 @@ import { TableName } from "@app/db/schemas"; import { TPkiCertificateTemplatesV2Insert } from "@app/db/schemas/pki-certificate-templates-v2"; import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; +import { + applyProcessedPermissionRulesToQuery, + type ProcessedPermissionRules +} from "@app/lib/knex/permission-filter-utils"; import { TCertificateTemplateV2, @@ -133,6 +137,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { limit?: number; search?: string; } = {}, + processedRules?: ProcessedPermissionRules, tx?: Knex ) => { try { @@ -146,6 +151,14 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { }); } + if (processedRules) { + query = applyProcessedPermissionRulesToQuery( + query, + TableName.PkiCertificateTemplateV2, + processedRules + ) as typeof query; + } + const certificateTemplatesV2 = await query.orderBy("createdAt", "desc").offset(offset).limit(limit); return certificateTemplatesV2.map((template: Record) => parseJsonFields(template)); @@ -159,6 +172,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { options: { search?: string; } = {}, + processedRules?: ProcessedPermissionRules, tx?: Knex ) => { try { @@ -172,6 +186,14 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { }); } + if (processedRules) { + query = applyProcessedPermissionRulesToQuery( + query, + TableName.PkiCertificateTemplateV2, + processedRules + ) as typeof query; + } + const result = await query.count("*").first(); return parseInt((result as unknown as { count: string }).count || "0", 10); } catch (error) { diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts index f73d516a6..3b8f7bc13 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts @@ -267,14 +267,22 @@ describe("CertificateTemplateV2Service", () => { limit: 20 }); - expect(mockCertificateTemplateV2DAL.findByProjectId).toHaveBeenCalledWith("project-123", { - offset: 0, - limit: 20, - search: undefined - }); - expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith("project-123", { - search: undefined - }); + expect(mockCertificateTemplateV2DAL.findByProjectId).toHaveBeenCalledWith( + "project-123", + { + offset: 0, + limit: 20, + search: undefined + }, + { allowRules: [], forbidRules: [] } + ); + expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith( + "project-123", + { + search: undefined + }, + { allowRules: [], forbidRules: [] } + ); expect(result).toEqual({ templates, totalCount }); }); @@ -291,14 +299,22 @@ describe("CertificateTemplateV2Service", () => { search: "web server" }); - expect(mockCertificateTemplateV2DAL.findByProjectId).toHaveBeenCalledWith("project-123", { - offset: 0, - limit: 20, - search: "web server" - }); - expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith("project-123", { - search: "web server" - }); + expect(mockCertificateTemplateV2DAL.findByProjectId).toHaveBeenCalledWith( + "project-123", + { + offset: 0, + limit: 20, + search: "web server" + }, + { allowRules: [], forbidRules: [] } + ); + expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith( + "project-123", + { + search: "web server" + }, + { allowRules: [], forbidRules: [] } + ); }); }); diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts index 656c12e64..5d24e4b8b 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts @@ -1,4 +1,4 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; import RE2 from "re2"; @@ -8,6 +8,7 @@ import { ProjectPermissionPkiTemplateActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -77,12 +78,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 +635,9 @@ export const certificateTemplateV2ServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiTemplateActions.Create, - ProjectPermissionSub.CertificateTemplates + subject(ProjectPermissionSub.CertificateTemplates, { + name: data.name + }) ); if (!data) { @@ -711,7 +714,9 @@ export const certificateTemplateV2ServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiTemplateActions.Edit, - ProjectPermissionSub.CertificateTemplates + subject(ProjectPermissionSub.CertificateTemplates, { + name: existingTemplate.name + }) ); const consolidatedData = { @@ -784,7 +789,9 @@ export const certificateTemplateV2ServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiTemplateActions.Read, - ProjectPermissionSub.CertificateTemplates + subject(ProjectPermissionSub.CertificateTemplates, { + name: template.name + }) ); } @@ -815,16 +822,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; }; @@ -864,13 +872,18 @@ export const certificateTemplateV2ServiceFactory = ({ ProjectPermissionSub.CertificateTemplates ); - const templates = await certificateTemplateV2DAL.findByProjectId(projectId, { - offset, - limit, - search - }); + const processedRules = getProcessedPermissionRules( + permission, + ProjectPermissionPkiTemplateActions.Read, + ProjectPermissionSub.CertificateTemplates + ); + const templates = await certificateTemplateV2DAL.findByProjectId( + projectId, + { offset, limit, search }, + processedRules + ); - const totalCount = await certificateTemplateV2DAL.countByProjectId(projectId, { search }); + const totalCount = await certificateTemplateV2DAL.countByProjectId(projectId, { search }, processedRules); return { templates, @@ -907,7 +920,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 20e5d3e1c..c20412537 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"; @@ -167,7 +167,9 @@ const validateProfileAndPermissions = async ( ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionCertificateProfileActions.IssueCert, - ProjectPermissionSub.CertificateProfiles + subject(ProjectPermissionSub.CertificateProfiles, { + slug: profile.slug + }) ); return profile; @@ -999,11 +1001,27 @@ export const certificateV3ServiceFactory = ({ }); } + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: profile.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + const canReadPrivateKey = permission.can( + ProjectPermissionCertificateActions.ReadPrivateKey, + ProjectPermissionSub.Certificates + ); + + const privateKeyForResponse = canReadPrivateKey ? selfSignedResult.privateKey.toString("utf8") : undefined; + return { certificate: selfSignedResult.certificate.toString("utf8"), issuingCaCertificate: "", certificateChain: selfSignedResult.certificate.toString("utf8"), - privateKey: selfSignedResult.privateKey.toString("utf8"), + privateKey: privateKeyForResponse, serialNumber: selfSignedResult.serialNumber, certificateId: certificateData.id, certificateRequestId, @@ -1100,11 +1118,28 @@ export const certificateV3ServiceFactory = ({ finalCertificateChain = removeRootCaFromChain(finalCertificateChain); } + // Check if user has permission to read private key + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: profile.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + const canReadPrivateKey = permission.can( + ProjectPermissionCertificateActions.ReadPrivateKey, + ProjectPermissionSub.Certificates + ); + + const privateKeyForResponse = canReadPrivateKey ? bufferToString(privateKey) : undefined; + return { certificate: bufferToString(certificate), issuingCaCertificate: bufferToString(issuingCaCertificate), certificateChain: finalCertificateChain, - privateKey: bufferToString(privateKey), + privateKey: privateKeyForResponse, serialNumber, certificateId: cert.id, certificateRequestId, @@ -1489,10 +1524,12 @@ export const certificateV3ServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateProfileActions.IssueCert, - ProjectPermissionSub.CertificateProfiles - ); + if (profile) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateProfileActions.IssueCert, + subject(ProjectPermissionSub.CertificateProfiles, { slug: profile.slug }) + ); + } } const issuerType = profile?.issuerType || (originalCert.caId ? IssuerType.CA : IssuerType.SELF_SIGNED); @@ -1869,7 +1906,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) { @@ -1972,7 +2013,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-dal.ts b/backend/src/services/certificate/certificate-dal.ts index 7af79319b..72cef90fa 100644 --- a/backend/src/services/certificate/certificate-dal.ts +++ b/backend/src/services/certificate/certificate-dal.ts @@ -4,6 +4,10 @@ import { TDbClient } from "@app/db"; import { TableName, TCertificates } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { + applyProcessedPermissionRulesToQuery, + type ProcessedPermissionRules +} from "@app/lib/knex/permission-filter-utils"; import { CertStatus } from "./certificate-types"; @@ -140,7 +144,8 @@ export const certificateDALFactory = (db: TDbClient) => { const findActiveCertificatesForSync = async ( filter: Partial, - options?: { limit?: number; offset?: number } + options?: { limit?: number; offset?: number }, + permissionFilters?: ProcessedPermissionRules ): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => { try { let query = db @@ -163,6 +168,10 @@ export const certificateDALFactory = (db: TDbClient) => { } }); + if (permissionFilters) { + query = applyProcessedPermissionRulesToQuery(query, TableName.Certificate, permissionFilters) as typeof query; + } + if (options?.offset) { query = query.offset(options.offset); } @@ -267,7 +276,8 @@ export const certificateDALFactory = (db: TDbClient) => { const findWithPrivateKeyInfo = async ( filter: Partial, - options?: { offset?: number; limit?: number; sort?: [string, "asc" | "desc"][] } + options?: { offset?: number; limit?: number; sort?: [string, "asc" | "desc"][] }, + permissionFilters?: ProcessedPermissionRules ): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => { try { let query = db @@ -287,6 +297,10 @@ export const certificateDALFactory = (db: TDbClient) => { } }); + if (permissionFilters) { + query = applyProcessedPermissionRulesToQuery(query, TableName.Certificate, permissionFilters) as typeof query; + } + if (options?.offset) { query = query.offset(options.offset); } 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-dal.ts b/backend/src/services/pki-sync/pki-sync-dal.ts index 460bbfc42..d04344b1e 100644 --- a/backend/src/services/pki-sync/pki-sync-dal.ts +++ b/backend/src/services/pki-sync/pki-sync-dal.ts @@ -4,6 +4,10 @@ import { TDbClient } from "@app/db"; import { TableName, TPkiSyncs } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex"; +import { + applyProcessedPermissionRulesToQuery, + type ProcessedPermissionRules +} from "@app/lib/knex/permission-filter-utils"; import { PkiSync } from "./pki-sync-enums"; @@ -45,13 +49,15 @@ const basePkiSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: PkiSyncF const basePkiSyncWithSubscriberQuery = ({ filter, db, - tx + tx, + processedRules }: { db: TDbClient; filter?: PkiSyncFindFilter; tx?: Knex; + processedRules?: ProcessedPermissionRules; }) => { - const query = (tx || db.replicaNode())(TableName.PkiSync) + let query = (tx || db.replicaNode())(TableName.PkiSync) .leftJoin(TableName.AppConnection, `${TableName.PkiSync}.connectionId`, `${TableName.AppConnection}.id`) .leftJoin(TableName.PkiSubscriber, `${TableName.PkiSync}.subscriberId`, `${TableName.PkiSubscriber}.id`) .select(selectAllTableCols(TableName.PkiSync)) @@ -82,6 +88,10 @@ const basePkiSyncWithSubscriberQuery = ({ void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.PkiSync, filter))); } + if (processedRules) { + query = applyProcessedPermissionRulesToQuery(query, TableName.PkiSync, processedRules) as typeof query; + } + return query; }; @@ -184,9 +194,18 @@ export const pkiSyncDALFactory = (db: TDbClient) => { } }; - const findByProjectIdWithSubscribers = async (projectId: string, tx?: Knex) => { + const findByProjectIdWithSubscribers = async ( + projectId: string, + processedRules?: ProcessedPermissionRules, + tx?: Knex + ) => { try { - const pkiSyncs = await basePkiSyncWithSubscriberQuery({ filter: { projectId }, db, tx }); + const pkiSyncs = await basePkiSyncWithSubscriberQuery({ + filter: { projectId }, + db, + tx, + processedRules + }); return pkiSyncs.map(expandPkiSyncWithSubscriber); } catch (error) { throw new DatabaseError({ error, name: "Find By Project ID With Subscribers - PKI Sync" }); diff --git a/backend/src/services/pki-sync/pki-sync-service.ts b/backend/src/services/pki-sync/pki-sync-service.ts index 02a76db2a..f5e574213 100644 --- a/backend/src/services/pki-sync/pki-sync-service.ts +++ b/backend/src/services/pki-sync/pki-sync-service.ts @@ -4,6 +4,7 @@ import { ActionProjectType, TCertificateSyncs } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionPkiSyncActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; @@ -145,9 +146,10 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.Create, - subscriber - ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: subscriber.name }) - : ProjectPermissionSub.PkiSyncs + subject(ProjectPermissionSub.PkiSyncs, { + subscriberName: subscriber?.name, + name + }) ); // Get the destination app type based on PKI sync destination @@ -235,9 +237,10 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.Edit, - currentSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: currentSubscriber.name }) - : ProjectPermissionSub.PkiSyncs + subject(ProjectPermissionSub.PkiSyncs, { + subscriberName: currentSubscriber?.name, + name: pkiSync.name + }) ); if (name && name !== pkiSync.name) { @@ -331,9 +334,10 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.Delete, - pkiSyncSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: pkiSyncSubscriber.name }) - : ProjectPermissionSub.PkiSyncs + subject(ProjectPermissionSub.PkiSyncs, { + subscriberName: pkiSyncSubscriber?.name, + name: pkiSync.name + }) ); return pkiSyncDAL.deleteById(id); @@ -354,7 +358,13 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs); - const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId); + const processedRules = getProcessedPermissionRules( + permission, + ProjectPermissionPkiSyncActions.Read, + ProjectPermissionSub.PkiSyncs + ); + + const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId, processedRules); if (certificateId) { const syncsWithCertificateInfo = await Promise.all( @@ -406,9 +416,10 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.Read, - findSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: findSubscriber.name }) - : ProjectPermissionSub.PkiSyncs + subject(ProjectPermissionSub.PkiSyncs, { + subscriberName: findSubscriber?.name, + name: pkiSync.name + }) ); const result = { @@ -442,9 +453,10 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.SyncCertificates, - syncSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: syncSubscriber.name }) - : ProjectPermissionSub.PkiSyncs + subject(ProjectPermissionSub.PkiSyncs, { + subscriberName: syncSubscriber?.name, + name: pkiSync.name + }) ); await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: id }); @@ -483,9 +495,10 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.ImportCertificates, - importSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: importSubscriber.name }) - : ProjectPermissionSub.PkiSyncs + subject(ProjectPermissionSub.PkiSyncs, { + subscriberName: importSubscriber?.name, + name: pkiSync.name + }) ); await pkiSyncQueue.queuePkiSyncImportCertificatesById({ syncId: id }); @@ -516,9 +529,10 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPkiSyncActions.RemoveCertificates, - removeSubscriber - ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: removeSubscriber.name }) - : ProjectPermissionSub.PkiSyncs + subject(ProjectPermissionSub.PkiSyncs, { + subscriberName: removeSubscriber?.name, + name: pkiSync.name + }) ); await pkiSyncQueue.queuePkiSyncRemoveCertificatesById({ syncId: id }); @@ -549,7 +563,18 @@ 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, + subject(ProjectPermissionSub.PkiSyncs, { + subscriberName: pkiSyncSubscriber?.name, + name: pkiSync.name + }) + ); await validateCertificatesProjectOwnership(certificateIds, pkiSync.projectId); @@ -588,7 +613,12 @@ 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 +656,12 @@ 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..cf1771aba 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, @@ -39,6 +40,7 @@ import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certific import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal"; import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal"; import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; +import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { DatabaseErrorCode } from "@app/lib/error-codes"; @@ -911,7 +913,7 @@ export const projectServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionCertificateAuthorityActions.Read, ProjectPermissionSub.CertificateAuthorities ); @@ -963,35 +965,38 @@ export const projectServiceFactory = ({ ProjectPermissionSub.Certificates ); + const regularFilters = { + projectId, + ...(friendlyName && { friendlyName }), + ...(commonName && { commonName }) + }; + const permissionFilters = getProcessedPermissionRules( + permission, + ProjectPermissionCertificateActions.Read, + ProjectPermissionSub.Certificates + ); + const certificates = forPkiSync - ? await certificateDAL.findActiveCertificatesForSync( - { - projectId, - ...(friendlyName && { friendlyName }), - ...(commonName && { commonName }) - }, - { offset, limit } - ) + ? await certificateDAL.findActiveCertificatesForSync(regularFilters, { offset, limit }, permissionFilters) : await certificateDAL.findWithPrivateKeyInfo( + regularFilters, { - projectId, - ...(friendlyName && { friendlyName }), - ...(commonName && { commonName }) + offset, + limit, + sort: [["notAfter", "desc"]] }, - { offset, limit, sort: [["notAfter", "desc"]] } + permissionFilters ); + const countFilter = { + projectId, + ...(regularFilters.friendlyName && { friendlyName: String(regularFilters.friendlyName) }), + ...(regularFilters.commonName && { commonName: String(regularFilters.commonName) }) + }; + const count = forPkiSync - ? await certificateDAL.countActiveCertificatesForSync({ - projectId, - friendlyName, - commonName - }) - : await certificateDAL.countCertificatesInProject({ - projectId, - friendlyName, - commonName - }); + ? await certificateDAL.countActiveCertificatesForSync(countFilter) + : await certificateDAL.countCertificatesInProject(countFilter); return { certificates, diff --git a/docs/docs.json b/docs/docs.json index be76a10c1..5a3d965fd 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -2493,8 +2493,6 @@ "pages": [ "api-reference/endpoints/certificates/list", "api-reference/endpoints/certificates/read", - "api-reference/endpoints/certificates/issue-certificate", - "api-reference/endpoints/certificates/sign-certificate", "api-reference/endpoints/certificates/renew", "api-reference/endpoints/certificates/update-config", "api-reference/endpoints/certificates/revoke", diff --git a/frontend/src/context/ProjectPermissionContext/index.tsx b/frontend/src/context/ProjectPermissionContext/index.tsx index 415a82641..9eb548c07 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, @@ -12,6 +13,7 @@ export { ProjectPermissionKmipActions, ProjectPermissionMemberActions, ProjectPermissionPkiSubscriberActions, + ProjectPermissionPkiSyncActions, ProjectPermissionPkiTemplateActions, ProjectPermissionSshHostActions, ProjectPermissionSub diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index a35451cd3..83eb2fecd 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,22 @@ 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; +}; + +export type CertificateProfileSubjectFields = { + slug: string; }; export type SecretRotationSubjectFields = { @@ -457,8 +490,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 +530,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..81feaf8e5 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, @@ -23,6 +24,7 @@ export { ProjectPermissionKmipActions, ProjectPermissionMemberActions, ProjectPermissionPkiSubscriberActions, + ProjectPermissionPkiSyncActions, ProjectPermissionPkiTemplateActions, ProjectPermissionSshHostActions, ProjectPermissionSub, 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..4f12c3950 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,80 @@ 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..2870f390d 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) => ( @@ -100,6 +104,7 @@ export const CaSection = () => { : "This action will prevent the CA from issuing new certificates." } onChange={(isOpen) => handlePopUpToggle("caStatus", isOpen)} + buttonText="Confirm" deleteKey="confirm" onDeleteApproved={() => onUpdateCaStatus(popUp?.caStatus?.data as { caId: string; status: CaStatus }) diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx index 27de9315a..b52ca3daa 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,23 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { cas && cas.length > 0 && cas.map((ca) => { + const canReadCa = permission.can( + ProjectPermissionCertificateAuthorityActions.Read, + subject(ProjectPermissionSub.CertificateAuthorities, { + name: ca.name + }) + ); + return ( + canReadCa && navigate({ to: "/organizations/$orgId/projects/cert-management/$projectId/ca/$caId", params: { @@ -118,8 +133,10 @@ 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..b8ba09d3e 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,29 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => { data && data.length > 0 && data.map((ca) => { + const canEditCa = permission.can( + ProjectPermissionCertificateAuthorityActions.Edit, + subject(ProjectPermissionSub.CertificateAuthorities, { + name: ca.name + }) + ); + return ( { + onClick={() => + canEditCa && handlePopUpOpen("ca", { caId: ca.id, name: ca.name, type: ca.type - }); - }} + }) + } > {ca.name} {ca.type} @@ -95,8 +114,10 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => { {(isAllowed) => ( { {(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && ( {(isAllowed) => ( { )} {(isAllowed) => ( - + {!certificateDetails ? (
{

Certificates

- - {(isAllowed) => ( -
+
+ + {(isAllowed) => ( + )} + + + {(isAllowed) => ( -
- )} - + )} + +
diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx index b4defeb3a..2be9e35e3 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx @@ -1,4 +1,5 @@ import { useMemo, useState } from "react"; +import { subject } from "@casl/ability"; import { faBan, faCertificate, @@ -40,8 +41,10 @@ import { import { Badge } from "@app/components/v3"; import { ProjectPermissionCertificateActions, + ProjectPermissionPkiSyncActions, ProjectPermissionSub, - useProject + useProject, + useProjectPermission } from "@app/context"; import { useUpdateRenewalConfig } from "@app/hooks/api"; import { caSupportsCapability } from "@app/hooks/api/ca/constants"; @@ -96,6 +99,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { const [perPage, setPerPage] = useState(PER_PAGE_INIT); const { currentProject } = useProject(); + const { permission } = useProjectPermission(); const { data, isPending } = useListWorkspaceCertificates({ projectId: currentProject?.id ?? "", offset: (page - 1) * perPage, @@ -207,55 +211,72 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { return "opacity-0 group-hover:opacity-100"; })()}`} > - {canShowAutoRenewalIcon && ( - { - if (hasFailed && certificate.renewalError) { - return `Auto-renewal failed: ${certificate.renewalError}`; - } - if (isAutoRenewalEnabled) { - const expiryDate = new Date(certificate.notAfter); - const now = new Date(); - const daysUntilExpiry = Math.ceil( - (expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000) - ); - const daysUntilRenewal = Math.max( - 0, - daysUntilExpiry - (certificate.renewBeforeDays || 0) - ); - return `Auto-renews in ${daysUntilRenewal}d`; - } - return "Set auto renewal"; - })()} - > - - - )} + return ( + { + if (hasFailed && certificate.renewalError) { + return `Auto-renewal failed: ${certificate.renewalError}`; + } + if (isAutoRenewalEnabled) { + const expiryDate = new Date(certificate.notAfter); + const now = new Date(); + const daysUntilExpiry = Math.ceil( + (expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000) + ); + const daysUntilRenewal = Math.max( + 0, + daysUntilExpiry - (certificate.renewBeforeDays || 0) + ); + return `Auto-renews in ${daysUntilRenewal}d`; + } + return "Set auto renewal"; + })()} + > + + + ); + })()}
@@ -268,7 +289,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { {(isAllowed) => ( { {isLegacyTemplatesEnabled && ( {(isAllowed) => ( { return ( {(isAllowed) => { return ( @@ -391,7 +427,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { return ( {(isAllowed) => ( { return ( {(isAllowed) => ( { {certificate.status === CertStatus.ACTIVE && !certificate.renewedByCertificateId && ( {(isAllowed) => ( { )} )} - {/* Only show revoke button if CA supports revocation */} + {/* Only show revoke button if CA supports revocation and certificate is not already revoked */} {(() => { const caType = caCapabilityMap[certificate.caId]; const supportsRevocation = !caType || caSupportsCapability(caType, CaCapability.REVOKE_CERTIFICATES); - if (!supportsRevocation) { + if (!supportsRevocation || isRevoked) { return null; } return ( {(isAllowed) => ( { })()} {(isAllowed) => ( { - // console.log("PKI Sync navigation:", { syncId: id, projectId }); - navigate({ - to: ROUTE_PATHS.CertManager.PkiSyncDetailsByIDPage.path, - params: { - syncId: id, - projectId, - orgId: currentOrg.id - } - }); - }} - className={twMerge( - "group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700", - syncStatus === PkiSyncStatus.Failed && "bg-red/5 hover:bg-red/10" - )} - key={`sync-${id}`} - > - - {`${destinationDetails.name} - - -
-
-

{name}

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

{destinationDetails.name}

-
- - - -
- {syncStatus && ( - - {lastSyncedAt && ( -
-
- -
Last Synced
-
-
- {format(new Date(lastSyncedAt), "yyyy-MM-dd, hh:mm aaa")} -
-
- )} - {failureMessage && ( -
-
- -
Failure Reason
-
-
- {failureMessage} -
-
- )} -
- ) : undefined + + {(isAllowed: boolean) => { + return ( + { + if (!isAllowed) { + return; } - > + // console.log("PKI Sync navigation:", { syncId: id, projectId }); + navigate({ + to: ROUTE_PATHS.CertManager.PkiSyncDetailsByIDPage.path, + params: { + syncId: id, + projectId, + orgId: currentOrg.id + } + }); + }} + className={twMerge( + "group h-10 transition-colors duration-100 hover:bg-mineshaft-700", + syncStatus === PkiSyncStatus.Failed && "bg-red/5 hover:bg-red/10", + isAllowed ? "cursor-pointer" : "cursor-not-allowed" + )} + key={`sync-${id}`} + > + + {`${destinationDetails.name} + +
- -
- - )} - {!isAutoSyncEnabled && ( - - - - {!syncStatus && "Auto-Sync Disabled"} - - - )} - {syncOption?.canImportCertificates && } - -
- - - - - - - - - - - } - 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..adb054149 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx @@ -53,7 +53,7 @@ type Props = { }; export const PkiSyncActionTriggers = ({ pkiSync }: Props) => { - const { destination, subscriberId, projectId, id } = pkiSync; + const { destination, projectId, id } = pkiSync; const navigate = useNavigate(); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ @@ -112,7 +112,8 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => { }, [updatePkiSyncMutation, id, projectId, pkiSync.isAutoSyncEnabled]); const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { - 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..761f52cdd 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,11 @@ 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 || "" + 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..651b03403 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection.tsx @@ -31,7 +31,7 @@ type Props = { }; export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props) => { - const { destination, subscriberId } = pkiSync; + const { destination } = pkiSync; const destinationDetails = PKI_SYNC_MAP[destination]; @@ -55,7 +55,8 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props) } const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { - 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..5ebfc659b 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 = ({ @@ -33,8 +34,7 @@ type Props = { }; export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => { - const { syncStatus, lastSyncMessage, lastSyncedAt, name, description, subscriberId, subscriber } = - pkiSync; + const { syncStatus, lastSyncMessage, lastSyncedAt, name, description, subscriber } = pkiSync; const failureMessage = useMemo(() => { if (syncStatus === PkiSyncStatus.Failed) { @@ -50,8 +50,11 @@ 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 || "" + 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..41745bc86 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,11 @@ 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 || "" + 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..891b4c3f0 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,11 @@ 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 || "" + 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..3abaee734 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,11 @@ 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 || "" + subscriberName: destinationName, + name: pkiSync.name }); return ( diff --git a/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx b/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx index 0653d44d0..c734e621e 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx @@ -2,8 +2,15 @@ import { useState } from "react"; import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; +import { PermissionDeniedBanner } from "@app/components/permissions"; import { ContentLoader, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; -import { useProject } from "@app/context"; +import { useProject, useProjectPermission } from "@app/context"; +import { + ProjectPermissionCertificateActions, + ProjectPermissionCertificateProfileActions, + ProjectPermissionPkiTemplateActions, + ProjectPermissionSub +} from "@app/context/ProjectPermissionContext/types"; import { ProjectType } from "@app/hooks/api/projects/types"; import { CertificateProfilesTab } from "./components/CertificateProfilesTab"; @@ -20,8 +27,22 @@ enum TabSections { export const PoliciesPage = () => { const { t } = useTranslation(); const { currentProject } = useProject(); + const { permission } = useProjectPermission(); const [activeTab, setActiveTab] = useState(TabSections.CertificateProfiles); + const canReadCertificateProfiles = permission.can( + ProjectPermissionCertificateProfileActions.Read, + ProjectPermissionSub.CertificateProfiles + ); + const canReadCertificateTemplates = permission.can( + ProjectPermissionPkiTemplateActions.Read, + ProjectPermissionSub.CertificateTemplates + ); + const canReadCertificates = permission.can( + ProjectPermissionCertificateActions.Read, + ProjectPermissionSub.Certificates + ); + if (!currentProject) { return ; } @@ -56,15 +77,19 @@ export const PoliciesPage = () => { - + {canReadCertificateProfiles ? : } - + {canReadCertificateTemplates ? ( + + ) : ( + + )} - + {canReadCertificates ? : }
diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/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); @@ -72,26 +70,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" }, @@ -152,50 +130,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={} - > - Request 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/CreateTemplateModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx index d55c1b5ef..1da9e8ebe 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx @@ -258,7 +258,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" }; }; - const { control, handleSubmit, reset, watch, setValue, formState } = useForm< + const { control, handleSubmit, reset, watch, setValue, formState, trigger } = useForm< FormData & { preset: TemplatePresetId } >({ resolver: zodResolver(templateSchema), @@ -286,10 +286,11 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" }; const watchedPreset = watch("preset") || TEMPLATE_PRESET_IDS.CUSTOM; - const handlePresetChange = (presetId: TemplatePresetId) => { + const handlePresetChange = async (presetId: TemplatePresetId) => { setValue("preset", presetId); if (presetId === TEMPLATE_PRESET_IDS.CUSTOM) { + await trigger(); return; } @@ -313,6 +314,8 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" if (selectedPreset.formData.keyAlgorithm) { setValue("keyAlgorithm", selectedPreset.formData.keyAlgorithm); } + + await trigger(); } }; 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..7fa49de53 --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/CertificatePermissionConditions.tsx @@ -0,0 +1,23 @@ +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/PermissionConditionHelpers.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PermissionConditionHelpers.tsx index f6c44b386..c8408d1cf 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PermissionConditionHelpers.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PermissionConditionHelpers.tsx @@ -36,6 +36,7 @@ export const renderOperatorSelectItems = (type: string) => { return ( <> Equal + Not Equal Glob Match In diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/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..566a5f93a 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,17 @@ 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.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.Renew]: z.boolean().optional(), + [ProjectPermissionCertificateAuthorityActions.SignIntermediate]: z.boolean().optional() }); const SecretPolicyActionSchema = z.object({ @@ -208,7 +219,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,7 +227,6 @@ 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({ @@ -225,7 +234,9 @@ const CertificateProfilePolicyActionSchema = z.object({ [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 +392,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 +418,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 +483,9 @@ type TConditionalFields = | ProjectPermissionSub.DynamicSecrets | ProjectPermissionSub.PkiSubscribers | ProjectPermissionSub.CertificateTemplates + | ProjectPermissionSub.CertificateAuthorities + | ProjectPermissionSub.Certificates + | ProjectPermissionSub.CertificateProfiles | ProjectPermissionSub.SshHosts | ProjectPermissionSub.SecretRotation | ProjectPermissionSub.Identity @@ -479,6 +507,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 || @@ -822,6 +853,31 @@ 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 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.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 +929,22 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { const canEdit = action.includes(ProjectPermissionCertificateActions.Edit); const canDelete = action.includes(ProjectPermissionCertificateActions.Delete); const canCreate = action.includes(ProjectPermissionCertificateActions.Create); + 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.ReadPrivateKey]: canReadPrivateKey, + [ProjectPermissionCertificateActions.Import]: canImport, + conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], + inverted + }); return; } @@ -1129,9 +1190,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 +1215,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { [ProjectPermissionPkiTemplateActions.Read]: action.includes( ProjectPermissionPkiTemplateActions.Read ), - [ProjectPermissionPkiTemplateActions.IssueCert]: action.includes( - ProjectPermissionPkiTemplateActions.IssueCert - ), [ProjectPermissionPkiTemplateActions.ListCerts]: action.includes( ProjectPermissionPkiTemplateActions.ListCerts ), @@ -1187,7 +1242,15 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { ), [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 +1259,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,10 +1615,15 @@ 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: "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]: { @@ -1563,7 +1631,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { actions: [ { label: "Read", value: ProjectPermissionCertificateActions.Read }, { 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 +1644,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 } ] }, @@ -1586,7 +1654,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { { 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 +1710,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 } ] }, diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx index e853715e0..de46579e8 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 ; }