diff --git a/backend/src/lib/casl/permission-filter-utils.ts b/backend/src/lib/casl/permission-filter-utils.ts index 1f074d1d6..35f519c33 100644 --- a/backend/src/lib/casl/permission-filter-utils.ts +++ b/backend/src/lib/casl/permission-filter-utils.ts @@ -41,7 +41,7 @@ interface MongoGlobFilter { * @param isInverted - Whether this rule is inverted (forbidden) * @returns Record of field names to arrays of filter configurations */ -export const buildPermissionFiltersFromConditions = (conditions: MongoQuery, isInverted = false): PermissionFilters => { +const buildPermissionFiltersFromConditions = (conditions: MongoQuery, isInverted = false): PermissionFilters => { const permissionFilters: PermissionFilters = {}; function addFilterToField(key: string, operator: string, value: unknown, isPattern: boolean) { @@ -184,83 +184,6 @@ export const buildPermissionFiltersFromConditions = (conditions: MongoQuery, isI return permissionFilters; }; -/** - * Convert CASL condition object to MongoDB-compatible database filter - * @param condition - CASL condition object - * @returns MongoDB filter object - */ -export function convertConditionToDbFilter(condition: Record): Record { - const dbFilter: Record = {}; - - for (const key in condition) { - if (Object.prototype.hasOwnProperty.call(condition, key)) { - const value = condition[key]; - - if (value && typeof value === "object" && !Array.isArray(value)) { - const operator = Object.keys(value)[0]; - const opValue = (value as Record)[operator]; - - switch (operator) { - case "$glob": { - // Convert "*.es" glob → regex /^.*\.es$/i - const regex = new RE2(`^${String(opValue).replace(/\*/g, ".*")}$`, "i"); - dbFilter[key] = { $regex: regex }; - break; - } - - case "$in": - dbFilter[key] = { $in: opValue }; - break; - - case "$eq": - dbFilter[key] = { $eq: opValue }; - break; - - default: - dbFilter[key] = value; - break; - } - } else { - dbFilter[key] = { $eq: value }; - } - } - } - - return dbFilter; -} - -/** - * Extract ALL conditions for a subject and action, - * converting them into DB-safe filter objects. - * @param ability - CASL MongoAbility instance - * @param action - Permission action to filter for - * @param subjectName - Permission subject to filter for - * @returns MongoDB filter object with $or conditions - */ -export function getDbFiltersForAbility( - ability: MongoAbility, - action: string, - subjectName: string -): Record { - const matchingRules = ability.rules.filter((rule: RawRuleOf) => { - const actionMatches = Array.isArray(rule.action) ? rule.action.includes(action) : rule.action === action; - const subjectMatches = Array.isArray(rule.subject) - ? rule.subject.includes(subjectName) - : rule.subject === subjectName; - return actionMatches && subjectMatches && rule.conditions; - }); - - const converted = matchingRules.map((rule: RawRuleOf) => - convertConditionToDbFilter(rule.conditions as Record) - ); - - if (converted.length === 0) { - return {}; - } - - return { $or: converted }; -} - /** * Extract permission filters for a subject and action, * converting them into ProcessedPermissionRules format for use with Knex queries. @@ -300,42 +223,3 @@ export function getProcessedPermissionRules( return { allowRules, forbidRules }; } - -/** - * Extract permission filters for a subject and action, - * converting them into PermissionFilters format for use with Knex queries. - * This is a simplified version that maintains backward compatibility. - * @param ability - CASL MongoAbility instance - * @param action - Permission action to filter for - * @param subjectName - Permission subject to filter for - * @returns PermissionFilters object for use with applyPermissionFiltersToQuery - */ -export function getPermissionFiltersForAbility( - ability: MongoAbility, - action: string, - subjectName: string -): PermissionFilters { - const processedRules = getProcessedPermissionRules(ability, action, subjectName); - - const mergedFilters: PermissionFilters = {}; - - processedRules.allowRules.forEach((rule) => { - Object.entries(rule).forEach(([key, filterConfigs]) => { - if (!mergedFilters[key]) { - mergedFilters[key] = []; - } - mergedFilters[key].push(...filterConfigs); - }); - }); - - processedRules.forbidRules.forEach((rule) => { - Object.entries(rule).forEach(([key, filterConfigs]) => { - if (!mergedFilters[key]) { - mergedFilters[key] = []; - } - mergedFilters[key].push(...filterConfigs); - }); - }); - - return mergedFilters; -} diff --git a/backend/src/lib/knex/permission-filter-utils.ts b/backend/src/lib/knex/permission-filter-utils.ts index 3b8f448ec..d6e0f92a4 100644 --- a/backend/src/lib/knex/permission-filter-utils.ts +++ b/backend/src/lib/knex/permission-filter-utils.ts @@ -15,86 +15,6 @@ export interface ProcessedPermissionRules { forbidRules: Array>>; } -/** - * Applies permission filters to a Knex query for any table - * @param query - The Knex query builder instance - * @param tableName - The name of the table to apply filters to - * @param permissionFilters - Record of field names to arrays of filter configurations - * @returns The modified query builder with permission filters applied - */ -export const applyPermissionFiltersToQuery = ( - originalQuery: Knex.QueryBuilder, - tableName: string, - permissionFilters?: PermissionFilters -): Knex.QueryBuilder => { - if (!permissionFilters) { - return originalQuery; - } - - let query = originalQuery; - - Object.entries(permissionFilters).forEach(([key, filterConfigs]) => { - filterConfigs.forEach((filterConfig) => { - if (filterConfig.value !== undefined && filterConfig.value !== null) { - const { operator, value, isPattern } = filterConfig; - const fieldName = `${tableName}.${key}`; - - switch (operator) { - case "=": - query = query.andWhere(fieldName, "=", value as string | number); - break; - case "!=": - query = query.andWhere(fieldName, "!=", value as string | number); - break; - case "LIKE": { - const likePattern = isPattern ? String(value).replace(new RE2("\\*", "g"), "%") : String(value); - query = query.andWhere(fieldName, "like", likePattern); - break; - } - case "NOT LIKE": { - const notLikePattern = isPattern ? String(value).replace(new RE2("\\*", "g"), "%") : String(value); - query = query.andWhere(fieldName, "not like", notLikePattern); - break; - } - case "IN": { - const inValues = Array.isArray(value) ? value : [value]; - query = query.andWhere(fieldName, "in", inValues as (string | number)[]); - break; - } - case "NOT IN": { - const notInValues = Array.isArray(value) ? value : [value]; - query = query.andWhere(fieldName, "not in", notInValues as (string | number)[]); - break; - } - case ">": - query = query.andWhere(fieldName, ">", value as string | number); - break; - case ">=": - query = query.andWhere(fieldName, ">=", value as string | number); - break; - case "<": - query = query.andWhere(fieldName, "<", value as string | number); - break; - case "<=": - query = query.andWhere(fieldName, "<=", value as string | number); - break; - case "IS NULL": - query = query.andWhere(fieldName, "is", null); - break; - case "IS NOT NULL": - query = query.andWhere(fieldName, "is not", null); - break; - default: - query = query.andWhere(fieldName, "=", value as string | number); - break; - } - } - }); - }); - - return query; -}; - /** * Applies a single filter configuration to a query * @param query - The Knex query builder instance @@ -204,65 +124,9 @@ export const applyProcessedPermissionRulesToQuery = ( if (processedRules.forbidRules.length > 0) { processedRules.forbidRules.forEach((forbidRule) => { - query = query.andWhere((forbidBuilder) => { - let hasConditions = false; - Object.entries(forbidRule).forEach(([key, filterConfigs]) => { - filterConfigs.forEach((filterConfig) => { - const negatedConfig = { ...filterConfig }; - - switch (filterConfig.operator) { - case "=": - negatedConfig.operator = "!="; - break; - case "!=": - negatedConfig.operator = "="; - break; - case "LIKE": - negatedConfig.operator = "NOT LIKE"; - break; - case "NOT LIKE": - negatedConfig.operator = "LIKE"; - break; - case "IN": - negatedConfig.operator = "NOT IN"; - break; - case "NOT IN": - negatedConfig.operator = "IN"; - break; - case ">": - negatedConfig.operator = "<="; - break; - case ">=": - negatedConfig.operator = "<"; - break; - case "<": - negatedConfig.operator = ">="; - break; - case "<=": - negatedConfig.operator = ">"; - break; - case "IS NULL": - negatedConfig.operator = "IS NOT NULL"; - break; - case "IS NOT NULL": - negatedConfig.operator = "IS NULL"; - break; - default: - negatedConfig.operator = "!="; - break; - } - - if (hasConditions) { - void forbidBuilder.orWhere((subBuilder) => { - applySingleFilter(subBuilder, tableName, key, negatedConfig); - }); - } else { - void forbidBuilder.where((subBuilder) => { - applySingleFilter(subBuilder, tableName, key, negatedConfig); - }); - hasConditions = true; - } - }); + Object.entries(forbidRule).forEach(([key, filterConfigs]) => { + filterConfigs.forEach((filterConfig) => { + applySingleFilter(query, tableName, key, filterConfig); }); }); }); diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index f14ff437f..397085171 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -176,7 +176,8 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId + actorOrgId: req.permission.orgId, + isInternal: true }); const caType = (ca?.externalCa?.type as CaType) ?? CaType.INTERNAL; useOrderFlow = caType !== CaType.INTERNAL; diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts index 0f39935ec..9616b3090 100644 --- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts @@ -7,7 +7,7 @@ import { TableName } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, CryptographyError, NotFoundError } from "@app/lib/errors"; -import { PermissionFilters } from "@app/lib/knex/permission-filter-utils"; +import { ProcessedPermissionRules } from "@app/lib/knex/permission-filter-utils"; import { OrgServiceActor } from "@app/lib/types"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; @@ -721,7 +721,7 @@ export const AcmeCertificateAuthorityFns = ({ permissionFilters }: { projectId: string; - permissionFilters?: PermissionFilters; + permissionFilters?: ProcessedPermissionRules; }) => { const cas = await certificateAuthorityDAL.findWithAssociatedCa( { diff --git a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts index 92be70560..7ebd66146 100644 --- a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts @@ -5,7 +5,7 @@ import RE2 from "re2"; import { TableName } from "@app/db/schemas"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; -import { PermissionFilters } from "@app/lib/knex/permission-filter-utils"; +import { ProcessedPermissionRules } from "@app/lib/knex/permission-filter-utils"; import { ms } from "@app/lib/ms"; import { OrgServiceActor } from "@app/lib/types"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; @@ -804,7 +804,7 @@ export const AzureAdCsCertificateAuthorityFns = ({ permissionFilters }: { projectId: string; - permissionFilters?: PermissionFilters; + permissionFilters?: ProcessedPermissionRules; }) => { const cas = await certificateAuthorityDAL.findWithAssociatedCa( { diff --git a/backend/src/services/certificate-authority/certificate-authority-dal.ts b/backend/src/services/certificate-authority/certificate-authority-dal.ts index 5809b04d9..6f621a515 100644 --- a/backend/src/services/certificate-authority/certificate-authority-dal.ts +++ b/backend/src/services/certificate-authority/certificate-authority-dal.ts @@ -4,7 +4,10 @@ import { TDbClient } from "@app/db"; import { CertificateAuthoritiesSchema, TableName, TCertificateAuthorities } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { buildFindFilter, ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex"; -import { applyPermissionFiltersToQuery, type PermissionFilters } from "@app/lib/knex/permission-filter-utils"; +import { + applyProcessedPermissionRulesToQuery, + type ProcessedPermissionRules +} from "@app/lib/knex/permission-filter-utils"; export type TCertificateAuthorityDALFactory = ReturnType; @@ -221,7 +224,7 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { const findWithAssociatedCa = async ( filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string; serialNumber?: string }, { offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt = {}, - permissionFilters?: PermissionFilters, + permissionFilters?: ProcessedPermissionRules, tx?: Knex ) => { try { @@ -271,7 +274,11 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { ); if (permissionFilters) { - query = applyPermissionFiltersToQuery(query, TableName.CertificateAuthority, permissionFilters) as typeof query; + query = applyProcessedPermissionRulesToQuery( + query, + TableName.CertificateAuthority, + permissionFilters + ) as typeof query; } if (limit) void query.limit(limit); diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 4e2ac1aba..a6dd4ce9e 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -6,7 +6,7 @@ import { ProjectPermissionCertificateAuthorityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { getPermissionFiltersForAbility } from "@app/lib/casl/permission-filter-utils"; +import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; @@ -337,7 +337,7 @@ export const certificateAuthorityServiceFactory = ({ ProjectPermissionSub.CertificateAuthorities ); - const permissionFilters = getPermissionFiltersForAbility( + const permissionFilters = getProcessedPermissionRules( permission, ProjectPermissionCertificateAuthorityActions.Read, ProjectPermissionSub.CertificateAuthorities @@ -711,34 +711,38 @@ export const certificateAuthorityServiceFactory = ({ actor, actorId, actorAuthMethod, - actorOrgId + actorOrgId, + isInternal }: { caId: string; actor: OrgServiceActor["type"]; actorId: string; actorAuthMethod: OrgServiceActor["authMethod"]; actorOrgId?: string; + isInternal?: boolean; }) => { const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); if (!ca) { throw new NotFoundError({ message: "CA not found" }); } - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: ca.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); + if (!isInternal) { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateAuthorityActions.Read, - subject(ProjectPermissionSub.CertificateAuthorities, { - name: ca.name - }) - ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateAuthorityActions.Read, + subject(ProjectPermissionSub.CertificateAuthorities, { + name: ca.name + }) + ); + } return ca; }; diff --git a/backend/src/services/certificate-profile/certificate-profile-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index 5bab780eb..3a8f99f1a 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -4,7 +4,10 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; -import { applyPermissionFiltersToQuery, type PermissionFilters } from "@app/lib/knex/permission-filter-utils"; +import { + applyProcessedPermissionRulesToQuery, + type ProcessedPermissionRules +} from "@app/lib/knex/permission-filter-utils"; import { EnrollmentType, @@ -277,7 +280,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { issuerType?: IssuerType; caId?: string; } = {}, - permissionFilters?: PermissionFilters, + processedRules?: ProcessedPermissionRules, tx?: Knex ): Promise => { try { @@ -356,11 +359,11 @@ export const certificateProfileDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeId") ); - if (permissionFilters) { - query = applyPermissionFiltersToQuery( + if (processedRules) { + query = applyProcessedPermissionRulesToQuery( query, TableName.PkiCertificateProfile, - permissionFilters + processedRules ) as typeof query; } @@ -442,7 +445,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { issuerType?: IssuerType; caId?: string; } = {}, - permissionFilters?: PermissionFilters, + processedRules?: ProcessedPermissionRules, tx?: Knex ): Promise => { try { @@ -470,11 +473,11 @@ export const certificateProfileDALFactory = (db: TDbClient) => { query = query.where({ issuerType }); } - if (permissionFilters) { - query = applyPermissionFiltersToQuery( + if (processedRules) { + query = applyProcessedPermissionRulesToQuery( query, TableName.PkiCertificateProfile, - permissionFilters + processedRules ) as typeof query; } diff --git a/backend/src/services/certificate-profile/certificate-profile-service.test.ts b/backend/src/services/certificate-profile/certificate-profile-service.test.ts index 0bc7a320b..d10188e6f 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.test.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.test.ts @@ -609,7 +609,7 @@ describe("CertificateProfileService", () => { caId: undefined, issuerType: undefined }, - {} + { allowRules: [], forbidRules: [] } ); }); @@ -634,7 +634,7 @@ describe("CertificateProfileService", () => { caId: "ca-123", issuerType: undefined }, - {} + { allowRules: [], forbidRules: [] } ); }); }); diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index 20da7891a..59e3afbd9 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -10,7 +10,7 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { buildUrl } from "@app/ee/services/pki-acme/pki-acme-fns"; -import { getPermissionFiltersForAbility } from "@app/lib/casl/permission-filter-utils"; +import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils"; import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; @@ -750,7 +750,7 @@ export const certificateProfileServiceFactory = ({ ProjectPermissionSub.CertificateProfiles ); - const permissionFilters = getPermissionFiltersForAbility( + const processedRules = getProcessedPermissionRules( permission, ProjectPermissionCertificateProfileActions.Read, ProjectPermissionSub.CertificateProfiles @@ -766,7 +766,7 @@ export const certificateProfileServiceFactory = ({ issuerType, caId }, - permissionFilters + processedRules ); const totalCount = await certificateProfileDAL.countByProjectId( @@ -777,7 +777,7 @@ export const certificateProfileServiceFactory = ({ issuerType, caId }, - permissionFilters + processedRules ); const convertedProfiles = await Promise.all( diff --git a/backend/src/services/certificate-request/certificate-request-service.test.ts b/backend/src/services/certificate-request/certificate-request-service.test.ts index 5e6b870bf..66d5c613b 100644 --- a/backend/src/services/certificate-request/certificate-request-service.test.ts +++ b/backend/src/services/certificate-request/certificate-request-service.test.ts @@ -10,6 +10,7 @@ import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionCertificateActions, + ProjectPermissionCertificateProfileActions, ProjectPermissionSet, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -79,8 +80,8 @@ describe("CertificateRequestService", () => { const mockPermission = { permission: createMongoAbility([ { - action: ProjectPermissionCertificateActions.Create, - subject: ProjectPermissionSub.Certificates + action: ProjectPermissionCertificateProfileActions.IssueCert, + subject: ProjectPermissionSub.CertificateProfiles } ]) }; diff --git a/backend/src/services/certificate-request/certificate-request-service.ts b/backend/src/services/certificate-request/certificate-request-service.ts index 15450ee6e..71129aba7 100644 --- a/backend/src/services/certificate-request/certificate-request-service.ts +++ b/backend/src/services/certificate-request/certificate-request-service.ts @@ -6,6 +6,7 @@ import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionCertificateActions, + ProjectPermissionCertificateProfileActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -110,8 +111,8 @@ export const certificateRequestServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionCertificateActions.Create, - ProjectPermissionSub.Certificates + ProjectPermissionCertificateProfileActions.IssueCert, + ProjectPermissionSub.CertificateProfiles ); } diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts index a2ed17a4c..4951986b1 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts @@ -5,7 +5,10 @@ import { TableName } from "@app/db/schemas"; import { TPkiCertificateTemplatesV2Insert } from "@app/db/schemas/pki-certificate-templates-v2"; import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; -import { applyPermissionFiltersToQuery, type PermissionFilters } from "@app/lib/knex/permission-filter-utils"; +import { + applyProcessedPermissionRulesToQuery, + type ProcessedPermissionRules +} from "@app/lib/knex/permission-filter-utils"; import { TCertificateTemplateV2, @@ -134,7 +137,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { limit?: number; search?: string; } = {}, - permissionFilters?: PermissionFilters, + processedRules?: ProcessedPermissionRules, tx?: Knex ) => { try { @@ -148,11 +151,11 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { }); } - if (permissionFilters) { - query = applyPermissionFiltersToQuery( + if (processedRules) { + query = applyProcessedPermissionRulesToQuery( query, TableName.PkiCertificateTemplateV2, - permissionFilters + processedRules ) as typeof query; } @@ -169,7 +172,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { options: { search?: string; } = {}, - permissionFilters?: PermissionFilters, + processedRules?: ProcessedPermissionRules, tx?: Knex ) => { try { @@ -183,11 +186,11 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { }); } - if (permissionFilters) { - query = applyPermissionFiltersToQuery( + if (processedRules) { + query = applyProcessedPermissionRulesToQuery( query, TableName.PkiCertificateTemplateV2, - permissionFilters + processedRules ) as typeof query; } diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts index 636bd4be7..3b8f7bc13 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts @@ -274,14 +274,14 @@ describe("CertificateTemplateV2Service", () => { limit: 20, search: undefined }, - {} + { allowRules: [], forbidRules: [] } ); expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith( "project-123", { search: undefined }, - {} + { allowRules: [], forbidRules: [] } ); expect(result).toEqual({ templates, totalCount }); }); @@ -306,14 +306,14 @@ describe("CertificateTemplateV2Service", () => { limit: 20, search: "web server" }, - {} + { allowRules: [], forbidRules: [] } ); expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith( "project-123", { search: "web server" }, - {} + { allowRules: [], forbidRules: [] } ); }); }); diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts index 36d77e4ff..5d24e4b8b 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts @@ -8,7 +8,7 @@ import { ProjectPermissionPkiTemplateActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { getPermissionFiltersForAbility } from "@app/lib/casl/permission-filter-utils"; +import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -872,7 +872,7 @@ export const certificateTemplateV2ServiceFactory = ({ ProjectPermissionSub.CertificateTemplates ); - const permissionFilters = getPermissionFiltersForAbility( + const processedRules = getProcessedPermissionRules( permission, ProjectPermissionPkiTemplateActions.Read, ProjectPermissionSub.CertificateTemplates @@ -880,10 +880,10 @@ export const certificateTemplateV2ServiceFactory = ({ const templates = await certificateTemplateV2DAL.findByProjectId( projectId, { offset, limit, search }, - permissionFilters + processedRules ); - const totalCount = await certificateTemplateV2DAL.countByProjectId(projectId, { search }, permissionFilters); + const totalCount = await certificateTemplateV2DAL.countByProjectId(projectId, { search }, processedRules); return { templates, diff --git a/backend/src/services/certificate/certificate-dal.ts b/backend/src/services/certificate/certificate-dal.ts index f61c46c28..72cef90fa 100644 --- a/backend/src/services/certificate/certificate-dal.ts +++ b/backend/src/services/certificate/certificate-dal.ts @@ -4,7 +4,10 @@ import { TDbClient } from "@app/db"; import { TableName, TCertificates } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; -import { applyPermissionFiltersToQuery, type PermissionFilters } from "@app/lib/knex/permission-filter-utils"; +import { + applyProcessedPermissionRulesToQuery, + type ProcessedPermissionRules +} from "@app/lib/knex/permission-filter-utils"; import { CertStatus } from "./certificate-types"; @@ -142,7 +145,7 @@ export const certificateDALFactory = (db: TDbClient) => { const findActiveCertificatesForSync = async ( filter: Partial, options?: { limit?: number; offset?: number }, - permissionFilters?: PermissionFilters + permissionFilters?: ProcessedPermissionRules ): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => { try { let query = db @@ -166,7 +169,7 @@ export const certificateDALFactory = (db: TDbClient) => { }); if (permissionFilters) { - query = applyPermissionFiltersToQuery(query, TableName.Certificate, permissionFilters) as typeof query; + query = applyProcessedPermissionRulesToQuery(query, TableName.Certificate, permissionFilters) as typeof query; } if (options?.offset) { @@ -274,7 +277,7 @@ export const certificateDALFactory = (db: TDbClient) => { const findWithPrivateKeyInfo = async ( filter: Partial, options?: { offset?: number; limit?: number; sort?: [string, "asc" | "desc"][] }, - permissionFilters?: PermissionFilters + permissionFilters?: ProcessedPermissionRules ): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => { try { let query = db @@ -295,7 +298,7 @@ export const certificateDALFactory = (db: TDbClient) => { }); if (permissionFilters) { - query = applyPermissionFiltersToQuery(query, TableName.Certificate, permissionFilters) as typeof query; + query = applyProcessedPermissionRulesToQuery(query, TableName.Certificate, permissionFilters) as typeof query; } if (options?.offset) { diff --git a/backend/src/services/pki-sync/pki-sync-dal.ts b/backend/src/services/pki-sync/pki-sync-dal.ts index 294ebff80..d04344b1e 100644 --- a/backend/src/services/pki-sync/pki-sync-dal.ts +++ b/backend/src/services/pki-sync/pki-sync-dal.ts @@ -4,7 +4,10 @@ import { TDbClient } from "@app/db"; import { TableName, TPkiSyncs } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex"; -import { applyPermissionFiltersToQuery, type PermissionFilters } from "@app/lib/knex/permission-filter-utils"; +import { + applyProcessedPermissionRulesToQuery, + type ProcessedPermissionRules +} from "@app/lib/knex/permission-filter-utils"; import { PkiSync } from "./pki-sync-enums"; @@ -47,12 +50,12 @@ const basePkiSyncWithSubscriberQuery = ({ filter, db, tx, - permissionFilters + processedRules }: { db: TDbClient; filter?: PkiSyncFindFilter; tx?: Knex; - permissionFilters?: PermissionFilters; + processedRules?: ProcessedPermissionRules; }) => { let query = (tx || db.replicaNode())(TableName.PkiSync) .leftJoin(TableName.AppConnection, `${TableName.PkiSync}.connectionId`, `${TableName.AppConnection}.id`) @@ -85,8 +88,8 @@ const basePkiSyncWithSubscriberQuery = ({ void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.PkiSync, filter))); } - if (permissionFilters) { - query = applyPermissionFiltersToQuery(query, TableName.PkiSync, permissionFilters) as typeof query; + if (processedRules) { + query = applyProcessedPermissionRulesToQuery(query, TableName.PkiSync, processedRules) as typeof query; } return query; @@ -193,7 +196,7 @@ export const pkiSyncDALFactory = (db: TDbClient) => { const findByProjectIdWithSubscribers = async ( projectId: string, - permissionFilters?: PermissionFilters, + processedRules?: ProcessedPermissionRules, tx?: Knex ) => { try { @@ -201,7 +204,7 @@ export const pkiSyncDALFactory = (db: TDbClient) => { filter: { projectId }, db, tx, - permissionFilters + processedRules }); return pkiSyncs.map(expandPkiSyncWithSubscriber); } catch (error) { diff --git a/backend/src/services/pki-sync/pki-sync-service.ts b/backend/src/services/pki-sync/pki-sync-service.ts index 2a4044208..f5e574213 100644 --- a/backend/src/services/pki-sync/pki-sync-service.ts +++ b/backend/src/services/pki-sync/pki-sync-service.ts @@ -4,7 +4,7 @@ import { ActionProjectType, TCertificateSyncs } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionPkiSyncActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { getPermissionFiltersForAbility } from "@app/lib/casl/permission-filter-utils"; +import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; @@ -358,13 +358,13 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs); - const permissionFilters = getPermissionFiltersForAbility( + const processedRules = getProcessedPermissionRules( permission, ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs ); - const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId, permissionFilters); + const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId, processedRules); if (certificateId) { const syncsWithCertificateInfo = await Promise.all( diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 2ea6503eb..cf1771aba 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -40,7 +40,7 @@ import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certific import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal"; import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal"; import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; -import { buildPermissionFiltersFromConditions, getDbFiltersForAbility } from "@app/lib/casl/permission-filter-utils"; +import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { DatabaseErrorCode } from "@app/lib/error-codes"; @@ -965,18 +965,16 @@ export const projectServiceFactory = ({ ProjectPermissionSub.Certificates ); - const filters = getDbFiltersForAbility( - permission, - ProjectPermissionCertificateActions.Read, - ProjectPermissionSub.Certificates - ); - const regularFilters = { projectId, ...(friendlyName && { friendlyName }), ...(commonName && { commonName }) }; - const permissionFilters = buildPermissionFiltersFromConditions(filters || {}); + const permissionFilters = getProcessedPermissionRules( + permission, + ProjectPermissionCertificateActions.Read, + ProjectPermissionSub.Certificates + ); const certificates = forPkiSync ? await certificateDAL.findActiveCertificatesForSync(regularFilters, { offset, limit }, permissionFilters) diff --git a/frontend/src/context/ProjectPermissionContext/index.tsx b/frontend/src/context/ProjectPermissionContext/index.tsx index cf3fe1b15..9eb548c07 100644 --- a/frontend/src/context/ProjectPermissionContext/index.tsx +++ b/frontend/src/context/ProjectPermissionContext/index.tsx @@ -13,6 +13,7 @@ export { ProjectPermissionKmipActions, ProjectPermissionMemberActions, ProjectPermissionPkiSubscriberActions, + ProjectPermissionPkiSyncActions, ProjectPermissionPkiTemplateActions, ProjectPermissionSshHostActions, ProjectPermissionSub diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx index 3bfd02466..81feaf8e5 100644 --- a/frontend/src/context/index.tsx +++ b/frontend/src/context/index.tsx @@ -24,6 +24,7 @@ export { ProjectPermissionKmipActions, ProjectPermissionMemberActions, ProjectPermissionPkiSubscriberActions, + ProjectPermissionPkiSyncActions, ProjectPermissionPkiTemplateActions, ProjectPermissionSshHostActions, ProjectPermissionSub, diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx index e31320d59..b8ba09d3e 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaTable.tsx @@ -72,8 +72,8 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => { data && data.length > 0 && data.map((ca) => { - const canReadCa = permission.can( - ProjectPermissionCertificateAuthorityActions.Read, + const canEditCa = permission.can( + ProjectPermissionCertificateAuthorityActions.Edit, subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name }) @@ -83,12 +83,12 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => { - canReadCa && + canEditCa && handlePopUpOpen("ca", { caId: ca.id, name: ca.name, diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx index e476f429d..2c3b00bc2 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx @@ -6,6 +6,7 @@ import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal } from "@app/components/v2"; import { ProjectPermissionCertificateActions, + ProjectPermissionCertificateProfileActions, ProjectPermissionSub, useProject } from "@app/context"; @@ -104,12 +105,12 @@ export const CertificatesSection = () => {

Certificates

- - {(isAllowed) => ( -
+
+ + {(isAllowed) => ( + )} + + + {(isAllowed) => ( -
- )} - + )} + +
diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx index bc562277d..2be9e35e3 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx @@ -41,8 +41,10 @@ import { import { Badge } from "@app/components/v3"; import { ProjectPermissionCertificateActions, + ProjectPermissionPkiSyncActions, ProjectPermissionSub, - useProject + useProject, + useProjectPermission } from "@app/context"; import { useUpdateRenewalConfig } from "@app/hooks/api"; import { caSupportsCapability } from "@app/hooks/api/ca/constants"; @@ -97,6 +99,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { const [perPage, setPerPage] = useState(PER_PAGE_INIT); const { currentProject } = useProject(); + const { permission } = useProjectPermission(); const { data, isPending } = useListWorkspaceCertificates({ projectId: currentProject?.id ?? "", offset: (page - 1) * perPage, @@ -208,55 +211,72 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { return "opacity-0 group-hover:opacity-100"; })()}`} > - {canShowAutoRenewalIcon && ( - { - if (hasFailed && certificate.renewalError) { - return `Auto-renewal failed: ${certificate.renewalError}`; - } - if (isAutoRenewalEnabled) { - const expiryDate = new Date(certificate.notAfter); - const now = new Date(); - const daysUntilExpiry = Math.ceil( - (expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000) - ); - const daysUntilRenewal = Math.max( - 0, - daysUntilExpiry - (certificate.renewBeforeDays || 0) - ); - return `Auto-renews in ${daysUntilRenewal}d`; - } - return "Set auto renewal"; - })()} - > - - - )} + return ( + { + if (hasFailed && certificate.renewalError) { + return `Auto-renewal failed: ${certificate.renewalError}`; + } + if (isAutoRenewalEnabled) { + const expiryDate = new Date(certificate.notAfter); + const now = new Date(); + const daysUntilExpiry = Math.ceil( + (expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000) + ); + const daysUntilRenewal = Math.max( + 0, + daysUntilExpiry - (certificate.renewBeforeDays || 0) + ); + return `Auto-renews in ${daysUntilRenewal}d`; + } + return "Set auto renewal"; + })()} + > + + + ); + })()}
@@ -481,13 +501,8 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { {certificate.status === CertStatus.ACTIVE && !certificate.renewedByCertificateId && ( {(isAllowed) => ( { )} )} - {/* Only show revoke button if CA supports revocation */} + {/* Only show revoke button if CA supports revocation and certificate is not already revoked */} {(() => { const caType = caCapabilityMap[certificate.caId]; const supportsRevocation = !caType || caSupportsCapability(caType, CaCapability.REVOKE_CERTIFICATES); - if (!supportsRevocation) { + if (!supportsRevocation || isRevoked) { return null; } diff --git a/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx b/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx index 0653d44d0..c734e621e 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx @@ -2,8 +2,15 @@ import { useState } from "react"; import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; +import { PermissionDeniedBanner } from "@app/components/permissions"; import { ContentLoader, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; -import { useProject } from "@app/context"; +import { useProject, useProjectPermission } from "@app/context"; +import { + ProjectPermissionCertificateActions, + ProjectPermissionCertificateProfileActions, + ProjectPermissionPkiTemplateActions, + ProjectPermissionSub +} from "@app/context/ProjectPermissionContext/types"; import { ProjectType } from "@app/hooks/api/projects/types"; import { CertificateProfilesTab } from "./components/CertificateProfilesTab"; @@ -20,8 +27,22 @@ enum TabSections { export const PoliciesPage = () => { const { t } = useTranslation(); const { currentProject } = useProject(); + const { permission } = useProjectPermission(); const [activeTab, setActiveTab] = useState(TabSections.CertificateProfiles); + const canReadCertificateProfiles = permission.can( + ProjectPermissionCertificateProfileActions.Read, + ProjectPermissionSub.CertificateProfiles + ); + const canReadCertificateTemplates = permission.can( + ProjectPermissionPkiTemplateActions.Read, + ProjectPermissionSub.CertificateTemplates + ); + const canReadCertificates = permission.can( + ProjectPermissionCertificateActions.Read, + ProjectPermissionSub.Certificates + ); + if (!currentProject) { return ; } @@ -56,15 +77,19 @@ export const PoliciesPage = () => { - + {canReadCertificateProfiles ? : } - + {canReadCertificateTemplates ? ( + + ) : ( + + )} - + {canReadCertificates ? : } diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx index d55c1b5ef..1da9e8ebe 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx @@ -258,7 +258,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" }; }; - const { control, handleSubmit, reset, watch, setValue, formState } = useForm< + const { control, handleSubmit, reset, watch, setValue, formState, trigger } = useForm< FormData & { preset: TemplatePresetId } >({ resolver: zodResolver(templateSchema), @@ -286,10 +286,11 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" }; const watchedPreset = watch("preset") || TEMPLATE_PRESET_IDS.CUSTOM; - const handlePresetChange = (presetId: TemplatePresetId) => { + const handlePresetChange = async (presetId: TemplatePresetId) => { setValue("preset", presetId); if (presetId === TEMPLATE_PRESET_IDS.CUSTOM) { + await trigger(); return; } @@ -313,6 +314,8 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" if (selectedPreset.formData.keyAlgorithm) { setValue("keyAlgorithm", selectedPreset.formData.keyAlgorithm); } + + await trigger(); } };