Merge pull request #4980 from Infisical/feat/PKI-67

feature: refine PKI access control permissions
This commit is contained in:
carlosmonastyrski
2025-12-04 23:21:07 -03:00
committed by GitHub
63 changed files with 2330 additions and 886 deletions
+1
View File
@@ -58,3 +58,4 @@ 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/aws-secrets-manager.mdx:private-key:62
docs/documentation/platform/pki/certificate-syncs/chef.mdx:private-key:61 docs/documentation/platform/pki/certificate-syncs/chef.mdx:private-key:61
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:246
backend/src/services/certificate-request/certificate-request-service.test.ts:private-key:248
@@ -4,7 +4,10 @@ import * as x509 from "@peculiar/x509";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; 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 { NotFoundError } from "@app/lib/errors";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { expandInternalCa } from "@app/services/certificate-authority/certificate-authority-fns"; import { expandInternalCa } from "@app/services/certificate-authority/certificate-authority-fns";
@@ -83,7 +86,7 @@ export const certificateAuthorityCrlServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionCertificateAuthorityActions.Read,
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateAuthorities
); );
@@ -5,6 +5,7 @@ import {
ProjectPermissionAppConnectionActions, ProjectPermissionAppConnectionActions,
ProjectPermissionAuditLogsActions, ProjectPermissionAuditLogsActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCertificateAuthorityActions,
ProjectPermissionCertificateProfileActions, ProjectPermissionCertificateProfileActions,
ProjectPermissionCmekActions, ProjectPermissionCmekActions,
ProjectPermissionCommitsActions, ProjectPermissionCommitsActions,
@@ -45,7 +46,6 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.Environments, ProjectPermissionSub.Environments,
ProjectPermissionSub.Tags, ProjectPermissionSub.Tags,
ProjectPermissionSub.IpAllowList, ProjectPermissionSub.IpAllowList,
ProjectPermissionSub.CertificateAuthorities,
ProjectPermissionSub.PkiAlerts, ProjectPermissionSub.PkiAlerts,
ProjectPermissionSub.PkiCollections, ProjectPermissionSub.PkiCollections,
ProjectPermissionSub.SshCertificateAuthorities, ProjectPermissionSub.SshCertificateAuthorities,
@@ -68,6 +68,18 @@ const buildAdminPermissionRules = () => {
can([ProjectPermissionAuditLogsActions.Read], ProjectPermissionSub.AuditLogs); can([ProjectPermissionAuditLogsActions.Read], ProjectPermissionSub.AuditLogs);
can(
[
ProjectPermissionCertificateAuthorityActions.Read,
ProjectPermissionCertificateAuthorityActions.Create,
ProjectPermissionCertificateAuthorityActions.Edit,
ProjectPermissionCertificateAuthorityActions.Delete,
ProjectPermissionCertificateAuthorityActions.Renew,
ProjectPermissionCertificateAuthorityActions.SignIntermediate
],
ProjectPermissionSub.CertificateAuthorities
);
can( can(
[ [
ProjectPermissionPkiTemplateActions.Read, ProjectPermissionPkiTemplateActions.Read,
@@ -96,7 +108,8 @@ const buildAdminPermissionRules = () => {
ProjectPermissionCertificateActions.Edit, ProjectPermissionCertificateActions.Edit,
ProjectPermissionCertificateActions.Create, ProjectPermissionCertificateActions.Create,
ProjectPermissionCertificateActions.Delete, ProjectPermissionCertificateActions.Delete,
ProjectPermissionCertificateActions.ReadPrivateKey ProjectPermissionCertificateActions.ReadPrivateKey,
ProjectPermissionCertificateActions.Import
], ],
ProjectPermissionSub.Certificates ProjectPermissionSub.Certificates
); );
@@ -461,7 +474,7 @@ const buildMemberPermissionRules = () => {
can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList); can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList);
// double check if all CRUD are needed for CA and Certificates // 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([ProjectPermissionPkiTemplateActions.Read], ProjectPermissionSub.CertificateTemplates);
can( can(
@@ -469,7 +482,8 @@ const buildMemberPermissionRules = () => {
ProjectPermissionCertificateActions.Read, ProjectPermissionCertificateActions.Read,
ProjectPermissionCertificateActions.Edit, ProjectPermissionCertificateActions.Edit,
ProjectPermissionCertificateActions.Create, ProjectPermissionCertificateActions.Create,
ProjectPermissionCertificateActions.Delete ProjectPermissionCertificateActions.Delete,
ProjectPermissionCertificateActions.Import
], ],
ProjectPermissionSub.Certificates ProjectPermissionSub.Certificates
); );
@@ -600,7 +614,7 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionAuditLogsActions.Read, ProjectPermissionSub.AuditLogs); can(ProjectPermissionAuditLogsActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities); can(ProjectPermissionCertificateAuthorityActions.Read, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates); can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionPkiTemplateActions.Read, ProjectPermissionSub.CertificateTemplates); can(ProjectPermissionPkiTemplateActions.Read, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
@@ -23,12 +23,22 @@ export enum ProjectPermissionCommitsActions {
PerformRollback = "perform-rollback" PerformRollback = "perform-rollback"
} }
export enum ProjectPermissionCertificateAuthorityActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
Renew = "renew",
SignIntermediate = "sign-intermediate"
}
export enum ProjectPermissionCertificateActions { export enum ProjectPermissionCertificateActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
Edit = "edit", Edit = "edit",
Delete = "delete", Delete = "delete",
ReadPrivateKey = "read-private-key" ReadPrivateKey = "read-private-key",
Import = "import"
} }
export enum ProjectPermissionSecretActions { export enum ProjectPermissionSecretActions {
@@ -292,7 +302,8 @@ export type SecretSyncSubjectFields = {
}; };
export type PkiSyncSubjectFields = { export type PkiSyncSubjectFields = {
subscriberName: string; subscriberName?: string;
name: string;
}; };
export type DynamicSecretSubjectFields = { export type DynamicSecretSubjectFields = {
@@ -332,6 +343,26 @@ export type PkiSubscriberSubjectFields = {
// (dangtony98): consider adding [commonName] as a subject field in the future // (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 = { export type AppConnectionSubjectFields = {
connectionId: string; connectionId: string;
}; };
@@ -399,8 +430,17 @@ export type ProjectPermissionSet =
ProjectPermissionIdentityActions, ProjectPermissionIdentityActions,
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields) ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
] ]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates] ProjectPermissionCertificateAuthorityActions,
(
| ProjectPermissionSub.CertificateAuthorities
| (ForcedSubject<ProjectPermissionSub.CertificateAuthorities> & CertificateAuthoritySubjectFields)
)
]
| [
ProjectPermissionCertificateActions,
ProjectPermissionSub.Certificates | (ForcedSubject<ProjectPermissionSub.Certificates> & CertificateSubjectFields)
]
| [ | [
ProjectPermissionPkiTemplateActions, ProjectPermissionPkiTemplateActions,
( (
@@ -454,7 +494,13 @@ export type ProjectPermissionSet =
ProjectPermissionSub.PamAccounts | (ForcedSubject<ProjectPermissionSub.PamAccounts> & PamAccountSubjectFields) ProjectPermissionSub.PamAccounts | (ForcedSubject<ProjectPermissionSub.PamAccounts> & PamAccountSubjectFields)
] ]
| [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions] | [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions]
| [ProjectPermissionCertificateProfileActions, ProjectPermissionSub.CertificateProfiles]; | [
ProjectPermissionCertificateProfileActions,
(
| ProjectPermissionSub.CertificateProfiles
| (ForcedSubject<ProjectPermissionSub.CertificateProfiles> & CertificateProfileSubjectFields)
)
];
const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'"; const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([ const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
@@ -572,6 +618,17 @@ const SecretSyncConditionV2Schema = z
const PkiSyncConditionSchema = z const PkiSyncConditionSchema = z
.object({ .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([ subscriberName: z.union([
z.string(), z.string(),
z z
@@ -698,6 +755,7 @@ const PkiTemplateConditionSchema = z
z z
.object({ .object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB], [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
}) })
@@ -749,6 +807,98 @@ const PamAccountConditionSchema = z
}) })
.partial(); .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 = [ const GeneralPermissionSchema = [
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), 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." "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({ z.object({
subject: z subject: z
.literal(ProjectPermissionSub.SshCertificateAuthorities) .literal(ProjectPermissionSub.SshCertificateAuthorities)
@@ -1130,7 +1268,30 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
inverted: z.boolean().optional().describe("Whether rule allows or forbids."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateProfileActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateProfileActions).describe(
"Describe what action an entity can take." "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 ...GeneralPermissionSchema
]); ]);
@@ -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<string, Array<PermissionFilterConfig>>;
export interface ProcessedPermissionRules {
allowRules: Array<Record<string, Array<PermissionFilterConfig>>>;
forbidRules: Array<Record<string, Array<PermissionFilterConfig>>>;
}
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<string, unknown>;
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<MongoAbility>) => {
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<Record<string, Array<PermissionFilterConfig>>> = [];
const forbidRules: Array<Record<string, Array<PermissionFilterConfig>>> = [];
matchingRules.forEach((rule: RawRuleOf<MongoAbility>) => {
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 };
}
@@ -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<string, Array<PermissionFilterConfig>>;
export interface ProcessedPermissionRules {
allowRules: Array<Record<string, Array<PermissionFilterConfig>>>;
forbidRules: Array<Record<string, Array<PermissionFilterConfig>>>;
}
/**
* 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"), "\\$&");
};
@@ -176,7 +176,8 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId,
isInternal: true
}); });
const caType = (ca?.externalCa?.type as CaType) ?? CaType.INTERNAL; const caType = (ca?.externalCa?.type as CaType) ?? CaType.INTERNAL;
useOrderFlow = caType !== CaType.INTERNAL; useOrderFlow = caType !== CaType.INTERNAL;
@@ -7,6 +7,7 @@ import { TableName } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, CryptographyError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, CryptographyError, NotFoundError } from "@app/lib/errors";
import { ProcessedPermissionRules } from "@app/lib/knex/permission-filter-utils";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
@@ -715,11 +716,21 @@ export const AcmeCertificateAuthorityFns = ({
return castDbEntryToAcmeCertificateAuthority(updatedCa); return castDbEntryToAcmeCertificateAuthority(updatedCa);
}; };
const listCertificateAuthorities = async ({ projectId }: { projectId: string }) => { const listCertificateAuthorities = async ({
const cas = await certificateAuthorityDAL.findWithAssociatedCa({ projectId,
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, permissionFilters
[`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.ACME }: {
}); 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); return cas.map(castDbEntryToAcmeCertificateAuthority);
}; };
@@ -5,6 +5,7 @@ import RE2 from "re2";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ProcessedPermissionRules } from "@app/lib/knex/permission-filter-utils";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
@@ -798,11 +799,21 @@ export const AzureAdCsCertificateAuthorityFns = ({
return castDbEntryToAzureAdCsCertificateAuthority(updatedCa); return castDbEntryToAzureAdCsCertificateAuthority(updatedCa);
}; };
const listCertificateAuthorities = async ({ projectId }: { projectId: string }) => { const listCertificateAuthorities = async ({
const cas = await certificateAuthorityDAL.findWithAssociatedCa({ projectId,
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, permissionFilters
[`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.AZURE_AD_CS }: {
}); 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); return cas.map(castDbEntryToAzureAdCsCertificateAuthority);
}; };
@@ -4,6 +4,10 @@ import { TDbClient } from "@app/db";
import { CertificateAuthoritiesSchema, TableName, TCertificateAuthorities } from "@app/db/schemas"; import { CertificateAuthoritiesSchema, TableName, TCertificateAuthorities } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex"; import { buildFindFilter, ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex";
import {
applyProcessedPermissionRulesToQuery,
type ProcessedPermissionRules
} from "@app/lib/knex/permission-filter-utils";
export type TCertificateAuthorityDALFactory = ReturnType<typeof certificateAuthorityDALFactory>; export type TCertificateAuthorityDALFactory = ReturnType<typeof certificateAuthorityDALFactory>;
@@ -220,10 +224,11 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => {
const findWithAssociatedCa = async ( const findWithAssociatedCa = async (
filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string; serialNumber?: string }, filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string; serialNumber?: string },
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TCertificateAuthorities> = {}, { offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TCertificateAuthorities> = {},
permissionFilters?: ProcessedPermissionRules,
tx?: Knex tx?: Knex
) => { ) => {
try { try {
const query = (tx || db.replicaNode())(TableName.CertificateAuthority) let query = (tx || db.replicaNode())(TableName.CertificateAuthority)
.leftJoin( .leftJoin(
TableName.InternalCertificateAuthority, TableName.InternalCertificateAuthority,
`${TableName.CertificateAuthority}.id`, `${TableName.CertificateAuthority}.id`,
@@ -268,6 +273,14 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => {
db.ref("appConnectionId").withSchema(TableName.ExternalCertificateAuthority).as("externalAppConnectionId") 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 (limit) void query.limit(limit);
if (offset) void query.offset(offset); if (offset) void query.offset(offset);
if (sort) { if (sort) {
@@ -1,8 +1,12 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType, TableName } from "@app/db/schemas"; import { ActionProjectType, TableName } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; 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 { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
@@ -142,8 +146,8 @@ export const certificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionCertificateAuthorityActions.Create,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, { name })
); );
if (type === CaType.INTERNAL) { if (type === CaType.INTERNAL) {
@@ -212,8 +216,8 @@ export const certificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionCertificateAuthorityActions.Read,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name })
); );
if (type === CaType.INTERNAL) { if (type === CaType.INTERNAL) {
@@ -227,6 +231,7 @@ export const certificateAuthorityServiceFactory = ({
id: certificateAuthority.id, id: certificateAuthority.id,
type, type,
enableDirectIssuance: certificateAuthority.enableDirectIssuance, enableDirectIssuance: certificateAuthority.enableDirectIssuance,
subject: ProjectPermissionSub.CertificateAuthorities,
name: certificateAuthority.name, name: certificateAuthority.name,
projectId: certificateAuthority.projectId, projectId: certificateAuthority.projectId,
configuration: certificateAuthority.internalCa, configuration: certificateAuthority.internalCa,
@@ -275,8 +280,8 @@ export const certificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionCertificateAuthorityActions.Read,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, { name: caName })
); );
if (type === CaType.INTERNAL) { if (type === CaType.INTERNAL) {
@@ -328,15 +333,25 @@ export const certificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionCertificateAuthorityActions.Read,
ProjectPermissionSub.CertificateAuthorities
);
const permissionFilters = getProcessedPermissionRules(
permission,
ProjectPermissionCertificateAuthorityActions.Read,
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateAuthorities
); );
if (type === CaType.INTERNAL) { if (type === CaType.INTERNAL) {
const cas = await certificateAuthorityDAL.findWithAssociatedCa({ const cas = await certificateAuthorityDAL.findWithAssociatedCa(
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, {
$notNull: [`${TableName.InternalCertificateAuthority}.id` as "id"] [`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId,
}); $notNull: [`${TableName.InternalCertificateAuthority}.id` as "id"]
},
{},
permissionFilters
);
return cas return cas
.filter((ca): ca is typeof ca & { internalCa: NonNullable<typeof ca.internalCa> } => Boolean(ca.internalCa)) .filter((ca): ca is typeof ca & { internalCa: NonNullable<typeof ca.internalCa> } => Boolean(ca.internalCa))
@@ -352,11 +367,11 @@ export const certificateAuthorityServiceFactory = ({
} }
if (type === CaType.ACME) { if (type === CaType.ACME) {
return acmeFns.listCertificateAuthorities({ projectId }); return acmeFns.listCertificateAuthorities({ projectId, permissionFilters });
} }
if (type === CaType.AZURE_AD_CS) { if (type === CaType.AZURE_AD_CS) {
return azureAdCsFns.listCertificateAuthorities({ projectId }); return azureAdCsFns.listCertificateAuthorities({ projectId, permissionFilters });
} }
throw new BadRequestError({ message: "Invalid certificate authority type" }); throw new BadRequestError({ message: "Invalid certificate authority type" });
@@ -383,8 +398,8 @@ export const certificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionCertificateAuthorityActions.Edit,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name })
); );
if (type === CaType.INTERNAL) { if (type === CaType.INTERNAL) {
@@ -459,8 +474,8 @@ export const certificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionCertificateAuthorityActions.Delete,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name })
); );
if (!certificateAuthority.internalCa?.id && type === CaType.INTERNAL) { if (!certificateAuthority.internalCa?.id && type === CaType.INTERNAL) {
@@ -524,8 +539,8 @@ export const certificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionCertificateAuthorityActions.Edit,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name })
); );
if (type === CaType.INTERNAL) { if (type === CaType.INTERNAL) {
@@ -606,8 +621,8 @@ export const certificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionCertificateAuthorityActions.Delete,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name })
); );
if (!certificateAuthority.internalCa?.id && type === CaType.INTERNAL) { if (!certificateAuthority.internalCa?.id && type === CaType.INTERNAL) {
@@ -662,6 +677,13 @@ export const certificateAuthorityServiceFactory = ({
actorAuthMethod: OrgServiceActor["authMethod"]; actorAuthMethod: OrgServiceActor["authMethod"];
actorOrgId?: string; 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({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
@@ -672,8 +694,10 @@ export const certificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionCertificateAuthorityActions.Read,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, {
name: certificateAuthority.name
})
); );
return azureAdCsFns.getTemplates({ return azureAdCsFns.getTemplates({
@@ -687,32 +711,38 @@ export const certificateAuthorityServiceFactory = ({
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
isInternal
}: { }: {
caId: string; caId: string;
actor: OrgServiceActor["type"]; actor: OrgServiceActor["type"];
actorId: string; actorId: string;
actorAuthMethod: OrgServiceActor["authMethod"]; actorAuthMethod: OrgServiceActor["authMethod"];
actorOrgId?: string; actorOrgId?: string;
isInternal?: boolean;
}) => { }) => {
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (!ca) { if (!ca) {
throw new NotFoundError({ message: "CA not found" }); throw new NotFoundError({ message: "CA not found" });
} }
const { permission } = await permissionService.getProjectPermission({ if (!isInternal) {
actor, const { permission } = await permissionService.getProjectPermission({
actorId, actor,
projectId: ca.projectId, actorId,
actorAuthMethod, projectId: ca.projectId,
actorOrgId, actorAuthMethod,
actionProjectType: ActionProjectType.CertificateManager actorOrgId,
}); actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionCertificateAuthorityActions.Read,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, {
); name: ca.name
})
);
}
return ca; return ca;
}; };
@@ -7,8 +7,8 @@ import { Knex } from "knex";
import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { import {
ProjectPermissionActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCertificateAuthorityActions,
ProjectPermissionCertificateProfileActions, ProjectPermissionCertificateProfileActions,
ProjectPermissionPkiTemplateActions, ProjectPermissionPkiTemplateActions,
ProjectPermissionSub ProjectPermissionSub
@@ -174,8 +174,8 @@ export const internalCertificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionCertificateAuthorityActions.Create,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, { name: commonName })
); );
} else { } else {
projectId = dto.projectId; projectId = dto.projectId;
@@ -357,8 +357,8 @@ export const internalCertificateAuthorityServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager actionProjectType: ActionProjectType.CertificateManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionCertificateAuthorityActions.Read,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
); );
return expandInternalCa(ca); return expandInternalCa(ca);
@@ -383,8 +383,8 @@ export const internalCertificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionCertificateAuthorityActions.Edit,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
); );
} }
@@ -416,8 +416,8 @@ export const internalCertificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionCertificateAuthorityActions.Delete,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
); );
await certificateAuthorityDAL.deleteById(ca.id); await certificateAuthorityDAL.deleteById(ca.id);
@@ -442,8 +442,8 @@ export const internalCertificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionCertificateAuthorityActions.Create,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
); );
if (ca.internalCa.type === InternalCaType.ROOT) if (ca.internalCa.type === InternalCaType.ROOT)
@@ -506,8 +506,8 @@ export const internalCertificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionCertificateAuthorityActions.Renew,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
); );
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
@@ -793,8 +793,8 @@ export const internalCertificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionCertificateAuthorityActions.Read,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
); );
const caCertChains = await getCaCertChains({ const caCertChains = await getCaCertChains({
@@ -830,8 +830,8 @@ export const internalCertificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionCertificateAuthorityActions.Read,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
); );
const { caCert, caCertChain, serialNumber } = await getCaCertChain({ const { caCert, caCertChain, serialNumber } = await getCaCertChain({
@@ -911,8 +911,8 @@ export const internalCertificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionCertificateAuthorityActions.SignIntermediate,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
); );
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
@@ -1059,8 +1059,8 @@ export const internalCertificateAuthorityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionCertificateAuthorityActions.Create,
ProjectPermissionSub.CertificateAuthorities subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
); );
if (ca.internalCa.parentCaId) { if (ca.internalCa.parentCaId) {
@@ -4,6 +4,10 @@ import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
import {
applyProcessedPermissionRulesToQuery,
type ProcessedPermissionRules
} from "@app/lib/knex/permission-filter-utils";
import { import {
EnrollmentType, EnrollmentType,
@@ -276,6 +280,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
issuerType?: IssuerType; issuerType?: IssuerType;
caId?: string; caId?: string;
} = {}, } = {},
processedRules?: ProcessedPermissionRules,
tx?: Knex tx?: Knex
): Promise<TCertificateProfile[] | TCertificateProfileWithConfigs[]> => { ): Promise<TCertificateProfile[] | TCertificateProfileWithConfigs[]> => {
try { try {
@@ -308,7 +313,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.issuerType`, issuerType); baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.issuerType`, issuerType);
} }
const query = baseQuery let query = baseQuery
.leftJoin( .leftJoin(
TableName.CertificateAuthority, TableName.CertificateAuthority,
`${TableName.PkiCertificateProfile}.caId`, `${TableName.PkiCertificateProfile}.caId`,
@@ -354,6 +359,14 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeId") db.ref("id").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeId")
); );
if (processedRules) {
query = applyProcessedPermissionRulesToQuery(
query,
TableName.PkiCertificateProfile,
processedRules
) as typeof query;
}
const results = (await query const results = (await query
.orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc") .orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc")
.offset(offset) .offset(offset)
@@ -432,6 +445,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
issuerType?: IssuerType; issuerType?: IssuerType;
caId?: string; caId?: string;
} = {}, } = {},
processedRules?: ProcessedPermissionRules,
tx?: Knex tx?: Knex
): Promise<number> => { ): Promise<number> => {
try { try {
@@ -459,6 +473,14 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
query = query.where({ issuerType }); query = query.where({ issuerType });
} }
if (processedRules) {
query = applyProcessedPermissionRulesToQuery(
query,
TableName.PkiCertificateProfile,
processedRules
) as typeof query;
}
const result = await query.count("*").first(); const result = await query.count("*").first();
return parseInt((result as unknown as { count: string }).count || "0", 10); return parseInt((result as unknown as { count: string }).count || "0", 10);
} catch (error) { } catch (error) {
@@ -169,7 +169,8 @@ describe("CertificateProfileService", () => {
const mockPermissionService = { const mockPermissionService = {
getProjectPermission: vi.fn().mockResolvedValue({ getProjectPermission: vi.fn().mockResolvedValue({
permission: { permission: {
throwUnlessCan: vi.fn() throwUnlessCan: vi.fn(),
rules: []
} }
}) })
} as unknown as Pick<TPermissionServiceFactory, "getProjectPermission">; } as unknown as Pick<TPermissionServiceFactory, "getProjectPermission">;
@@ -598,13 +599,18 @@ describe("CertificateProfileService", () => {
expect(result.profiles).toEqual(mockProfiles); expect(result.profiles).toEqual(mockProfiles);
expect(result.totalCount).toBe(1); expect(result.totalCount).toBe(1);
expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith("project-123", { expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith(
offset: 0, "project-123",
limit: 20, {
search: undefined, offset: 0,
enrollmentType: undefined, limit: 20,
caId: undefined search: undefined,
}); enrollmentType: undefined,
caId: undefined,
issuerType: undefined
},
{ allowRules: [], forbidRules: [] }
);
}); });
it("should list profiles with filters", async () => { it("should list profiles with filters", async () => {
@@ -618,13 +624,18 @@ describe("CertificateProfileService", () => {
caId: "ca-123" caId: "ca-123"
}); });
expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith("project-123", { expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith(
offset: 10, "project-123",
limit: 5, {
search: "test", offset: 10,
enrollmentType: EnrollmentType.API, limit: 5,
caId: "ca-123" search: "test",
}); enrollmentType: EnrollmentType.API,
caId: "ca-123",
issuerType: undefined
},
{ allowRules: [], forbidRules: [] }
);
}); });
}); });
@@ -1,4 +1,4 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
@@ -10,6 +10,7 @@ import {
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { buildUrl } from "@app/ee/services/pki-acme/pki-acme-fns"; 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 { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
@@ -299,7 +300,9 @@ export const certificateProfileServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.Create, ProjectPermissionCertificateProfileActions.Create,
ProjectPermissionSub.CertificateProfiles subject(ProjectPermissionSub.CertificateProfiles, {
slug: data.slug
})
); );
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
@@ -454,7 +457,9 @@ export const certificateProfileServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.Edit, ProjectPermissionCertificateProfileActions.Edit,
ProjectPermissionSub.CertificateProfiles subject(ProjectPermissionSub.CertificateProfiles, {
slug: existingProfile.slug
})
); );
if (data.certificateTemplateId) { if (data.certificateTemplateId) {
@@ -577,7 +582,9 @@ export const certificateProfileServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.Read, ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionSub.CertificateProfiles subject(ProjectPermissionSub.CertificateProfiles, {
slug: profile.slug
})
); );
const converted = convertDalToService(profile); const converted = convertDalToService(profile);
@@ -613,7 +620,9 @@ export const certificateProfileServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.Read, ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionSub.CertificateProfiles subject(ProjectPermissionSub.CertificateProfiles, {
slug: profile.slug
})
); );
if (profile.estConfig && profile.estConfig.caChain) { if (profile.estConfig && profile.estConfig.caChain) {
@@ -687,7 +696,9 @@ export const certificateProfileServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.Read, ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionSub.CertificateProfiles subject(ProjectPermissionSub.CertificateProfiles, {
slug
})
); );
const profile = await certificateProfileDAL.findBySlugAndProjectId(slug, projectId); const profile = await certificateProfileDAL.findBySlugAndProjectId(slug, projectId);
@@ -739,21 +750,35 @@ export const certificateProfileServiceFactory = ({
ProjectPermissionSub.CertificateProfiles ProjectPermissionSub.CertificateProfiles
); );
const profiles = await certificateProfileDAL.findByProjectId(projectId, { const processedRules = getProcessedPermissionRules(
offset, permission,
limit, ProjectPermissionCertificateProfileActions.Read,
search, ProjectPermissionSub.CertificateProfiles
enrollmentType, );
issuerType,
caId
});
const totalCount = await certificateProfileDAL.countByProjectId(projectId, { const profiles = await certificateProfileDAL.findByProjectId(
search, projectId,
enrollmentType, {
issuerType, offset,
caId limit,
}); search,
enrollmentType,
issuerType,
caId
},
processedRules
);
const totalCount = await certificateProfileDAL.countByProjectId(
projectId,
{
search,
enrollmentType,
issuerType,
caId
},
processedRules
);
const convertedProfiles = await Promise.all( const convertedProfiles = await Promise.all(
profiles.map(async (profile) => { profiles.map(async (profile) => {
@@ -838,7 +863,9 @@ export const certificateProfileServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.Delete, ProjectPermissionCertificateProfileActions.Delete,
ProjectPermissionSub.CertificateProfiles subject(ProjectPermissionSub.CertificateProfiles, {
slug: profile.slug
})
); );
const deletedProfile = await certificateProfileDAL.deleteById(profileId); const deletedProfile = await certificateProfileDAL.deleteById(profileId);
@@ -884,7 +911,9 @@ export const certificateProfileServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.Read, ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionSub.CertificateProfiles subject(ProjectPermissionSub.CertificateProfiles, {
slug: profile.slug
})
); );
const certificates = await certificateProfileDAL.getCertificatesByProfile(profileId, { const certificates = await certificateProfileDAL.getCertificatesByProfile(profileId, {
@@ -925,17 +954,9 @@ export const certificateProfileServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.Read, ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionSub.CertificateProfiles subject(ProjectPermissionSub.CertificateProfiles, {
); slug: profile.slug
})
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.ReadPrivateKey,
ProjectPermissionSub.Certificates
); );
const cert = await certificateProfileDAL.getLatestActiveCertificateForProfile(profileId); const cert = await certificateProfileDAL.getLatestActiveCertificateForProfile(profileId);
@@ -944,6 +965,24 @@ export const certificateProfileServiceFactory = ({
return null; 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 certBody = await certificateBodyDAL.findOne({ certId: cert.id });
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({ const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
@@ -1037,7 +1076,9 @@ export const certificateProfileServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.Read, ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionSub.CertificateProfiles subject(ProjectPermissionSub.CertificateProfiles, {
slug: profile.slug
})
); );
} }
@@ -1088,7 +1129,9 @@ export const certificateProfileServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret, ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret,
ProjectPermissionSub.CertificateProfiles subject(ProjectPermissionSub.CertificateProfiles, {
slug: profile.slug
})
); );
if (profile.enrollmentType !== EnrollmentType.ACME) { if (profile.enrollmentType !== EnrollmentType.ACME) {
@@ -10,6 +10,7 @@ import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { import {
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCertificateProfileActions,
ProjectPermissionSet, ProjectPermissionSet,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
@@ -79,8 +80,8 @@ describe("CertificateRequestService", () => {
const mockPermission = { const mockPermission = {
permission: createMongoAbility<ProjectPermissionSet>([ permission: createMongoAbility<ProjectPermissionSet>([
{ {
action: ProjectPermissionCertificateActions.Create, action: ProjectPermissionCertificateProfileActions.IssueCert,
subject: ProjectPermissionSub.Certificates subject: ProjectPermissionSub.CertificateProfiles
} }
]) ])
}; };
@@ -224,6 +225,10 @@ describe("CertificateRequestService", () => {
{ {
action: ProjectPermissionCertificateActions.Read, action: ProjectPermissionCertificateActions.Read,
subject: ProjectPermissionSub.Certificates 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 = { const mockPermission = {
permission: createMongoAbility<ProjectPermissionSet>([ permission: createMongoAbility<ProjectPermissionSet>([
{ {
@@ -348,7 +353,6 @@ describe("CertificateRequestService", () => {
(mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission);
(mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithCert); (mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithCert);
(mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody); (mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody);
(mockCertificateService.getCertPrivateKey as any).mockRejectedValue(new Error("Private key access denied"));
const result = await service.getCertificateFromRequest(mockGetData); const result = await service.getCertificateFromRequest(mockGetData);
@@ -362,8 +366,68 @@ describe("CertificateRequestService", () => {
actorAuthMethod: AuthMethod.EMAIL, actorAuthMethod: AuthMethod.EMAIL,
actorOrgId: "550e8400-e29b-41d4-a716-446655440002" 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<ProjectPermissionSet>([
{
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({ expect(mockCertificateService.getCertPrivateKey).toHaveBeenCalledWith({
id: "550e8400-e29b-41d4-a716-446655440008", id: "550e8400-e29b-41d4-a716-446655440009",
actor: ActorType.USER, actor: ActorType.USER,
actorId: "550e8400-e29b-41d4-a716-446655440001", actorId: "550e8400-e29b-41d4-a716-446655440001",
actorAuthMethod: AuthMethod.EMAIL, actorAuthMethod: AuthMethod.EMAIL,
@@ -6,6 +6,7 @@ import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { import {
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCertificateProfileActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -110,8 +111,8 @@ export const certificateRequestServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Create, ProjectPermissionCertificateProfileActions.IssueCert,
ProjectPermissionSub.Certificates ProjectPermissionSub.CertificateProfiles
); );
} }
@@ -217,20 +218,25 @@ export const certificateRequestServiceFactory = ({
actorOrgId 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; let privateKey: string | null = null;
try { if (canReadPrivateKey) {
const certPrivateKey = await certificateService.getCertPrivateKey({ try {
id: certificateRequest.certificate.id, const certPrivateKey = await certificateService.getCertPrivateKey({
actor, id: certificateRequest.certificate.id,
actorId, actor,
actorAuthMethod, actorId,
actorOrgId actorAuthMethod,
}); actorOrgId
privateKey = certPrivateKey.certPrivateKey; });
} catch (error) { privateKey = certPrivateKey.certPrivateKey;
// Private key access denied - continue without it } catch (error) {
privateKey = null; privateKey = null;
}
} }
return { return {
@@ -5,6 +5,10 @@ import { TableName } from "@app/db/schemas";
import { TPkiCertificateTemplatesV2Insert } from "@app/db/schemas/pki-certificate-templates-v2"; import { TPkiCertificateTemplatesV2Insert } from "@app/db/schemas/pki-certificate-templates-v2";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
import {
applyProcessedPermissionRulesToQuery,
type ProcessedPermissionRules
} from "@app/lib/knex/permission-filter-utils";
import { import {
TCertificateTemplateV2, TCertificateTemplateV2,
@@ -133,6 +137,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
limit?: number; limit?: number;
search?: string; search?: string;
} = {}, } = {},
processedRules?: ProcessedPermissionRules,
tx?: Knex tx?: Knex
) => { ) => {
try { 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); const certificateTemplatesV2 = await query.orderBy("createdAt", "desc").offset(offset).limit(limit);
return certificateTemplatesV2.map((template: Record<string, unknown>) => parseJsonFields(template)); return certificateTemplatesV2.map((template: Record<string, unknown>) => parseJsonFields(template));
@@ -159,6 +172,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
options: { options: {
search?: string; search?: string;
} = {}, } = {},
processedRules?: ProcessedPermissionRules,
tx?: Knex tx?: Knex
) => { ) => {
try { 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(); const result = await query.count("*").first();
return parseInt((result as unknown as { count: string }).count || "0", 10); return parseInt((result as unknown as { count: string }).count || "0", 10);
} catch (error) { } catch (error) {
@@ -267,14 +267,22 @@ describe("CertificateTemplateV2Service", () => {
limit: 20 limit: 20
}); });
expect(mockCertificateTemplateV2DAL.findByProjectId).toHaveBeenCalledWith("project-123", { expect(mockCertificateTemplateV2DAL.findByProjectId).toHaveBeenCalledWith(
offset: 0, "project-123",
limit: 20, {
search: undefined offset: 0,
}); limit: 20,
expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith("project-123", { search: undefined
search: undefined },
}); { allowRules: [], forbidRules: [] }
);
expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith(
"project-123",
{
search: undefined
},
{ allowRules: [], forbidRules: [] }
);
expect(result).toEqual({ templates, totalCount }); expect(result).toEqual({ templates, totalCount });
}); });
@@ -291,14 +299,22 @@ describe("CertificateTemplateV2Service", () => {
search: "web server" search: "web server"
}); });
expect(mockCertificateTemplateV2DAL.findByProjectId).toHaveBeenCalledWith("project-123", { expect(mockCertificateTemplateV2DAL.findByProjectId).toHaveBeenCalledWith(
offset: 0, "project-123",
limit: 20, {
search: "web server" offset: 0,
}); limit: 20,
expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith("project-123", { search: "web server"
search: "web server" },
}); { allowRules: [], forbidRules: [] }
);
expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith(
"project-123",
{
search: "web server"
},
{ allowRules: [], forbidRules: [] }
);
}); });
}); });
@@ -1,4 +1,4 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import RE2 from "re2"; import RE2 from "re2";
@@ -8,6 +8,7 @@ import {
ProjectPermissionPkiTemplateActions, ProjectPermissionPkiTemplateActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
@@ -77,12 +78,12 @@ export const certificateTemplateV2ServiceFactory = ({
}; };
const validateSubjectAttributePolicy = ( 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 // Validate each subject attribute policy
for (const attr of subject) { for (const attr of subjectAttributes) {
// Ensure at least one field is provided // Ensure at least one field is provided
if (!attr.allowed && !attr.required && !attr.denied) { if (!attr.allowed && !attr.required && !attr.denied) {
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
@@ -634,7 +635,9 @@ export const certificateTemplateV2ServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Create, ProjectPermissionPkiTemplateActions.Create,
ProjectPermissionSub.CertificateTemplates subject(ProjectPermissionSub.CertificateTemplates, {
name: data.name
})
); );
if (!data) { if (!data) {
@@ -711,7 +714,9 @@ export const certificateTemplateV2ServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Edit, ProjectPermissionPkiTemplateActions.Edit,
ProjectPermissionSub.CertificateTemplates subject(ProjectPermissionSub.CertificateTemplates, {
name: existingTemplate.name
})
); );
const consolidatedData = { const consolidatedData = {
@@ -784,7 +789,9 @@ export const certificateTemplateV2ServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Read, ProjectPermissionPkiTemplateActions.Read,
ProjectPermissionSub.CertificateTemplates subject(ProjectPermissionSub.CertificateTemplates, {
name: template.name
})
); );
} }
@@ -815,16 +822,17 @@ export const certificateTemplateV2ServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager actionProjectType: ActionProjectType.CertificateManager
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Read,
ProjectPermissionSub.CertificateTemplates
);
const template = await certificateTemplateV2DAL.findByNameAndProjectId(slug, projectId); const template = await certificateTemplateV2DAL.findByNameAndProjectId(slug, projectId);
if (!template) { if (!template) {
throw new NotFoundError({ message: "Certificate template not found" }); throw new NotFoundError({ message: "Certificate template not found" });
} }
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Read,
subject(ProjectPermissionSub.CertificateTemplates, {
name: template.name
})
);
return template; return template;
}; };
@@ -864,13 +872,18 @@ export const certificateTemplateV2ServiceFactory = ({
ProjectPermissionSub.CertificateTemplates ProjectPermissionSub.CertificateTemplates
); );
const templates = await certificateTemplateV2DAL.findByProjectId(projectId, { const processedRules = getProcessedPermissionRules(
offset, permission,
limit, ProjectPermissionPkiTemplateActions.Read,
search 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 { return {
templates, templates,
@@ -907,7 +920,9 @@ export const certificateTemplateV2ServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Delete, ProjectPermissionPkiTemplateActions.Delete,
ProjectPermissionSub.CertificateTemplates subject(ProjectPermissionSub.CertificateTemplates, {
name: template.name
})
); );
const isInUse = await certificateTemplateV2DAL.isTemplateInUse(templateId); const isInUse = await certificateTemplateV2DAL.isTemplateInUse(templateId);
@@ -1,4 +1,4 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
import RE2 from "re2"; import RE2 from "re2";
@@ -167,7 +167,9 @@ const validateProfileAndPermissions = async (
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.IssueCert, ProjectPermissionCertificateProfileActions.IssueCert,
ProjectPermissionSub.CertificateProfiles subject(ProjectPermissionSub.CertificateProfiles, {
slug: profile.slug
})
); );
return profile; 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 { return {
certificate: selfSignedResult.certificate.toString("utf8"), certificate: selfSignedResult.certificate.toString("utf8"),
issuingCaCertificate: "", issuingCaCertificate: "",
certificateChain: selfSignedResult.certificate.toString("utf8"), certificateChain: selfSignedResult.certificate.toString("utf8"),
privateKey: selfSignedResult.privateKey.toString("utf8"), privateKey: privateKeyForResponse,
serialNumber: selfSignedResult.serialNumber, serialNumber: selfSignedResult.serialNumber,
certificateId: certificateData.id, certificateId: certificateData.id,
certificateRequestId, certificateRequestId,
@@ -1100,11 +1118,28 @@ export const certificateV3ServiceFactory = ({
finalCertificateChain = removeRootCaFromChain(finalCertificateChain); 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 { return {
certificate: bufferToString(certificate), certificate: bufferToString(certificate),
issuingCaCertificate: bufferToString(issuingCaCertificate), issuingCaCertificate: bufferToString(issuingCaCertificate),
certificateChain: finalCertificateChain, certificateChain: finalCertificateChain,
privateKey: bufferToString(privateKey), privateKey: privateKeyForResponse,
serialNumber, serialNumber,
certificateId: cert.id, certificateId: cert.id,
certificateRequestId, certificateRequestId,
@@ -1489,10 +1524,12 @@ export const certificateV3ServiceFactory = ({
actionProjectType: ActionProjectType.CertificateManager actionProjectType: ActionProjectType.CertificateManager
}); });
ForbiddenError.from(permission).throwUnlessCan( if (profile) {
ProjectPermissionCertificateProfileActions.IssueCert, ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSub.CertificateProfiles ProjectPermissionCertificateProfileActions.IssueCert,
); subject(ProjectPermissionSub.CertificateProfiles, { slug: profile.slug })
);
}
} }
const issuerType = profile?.issuerType || (originalCert.caId ? IssuerType.CA : IssuerType.SELF_SIGNED); const issuerType = profile?.issuerType || (originalCert.caId ? IssuerType.CA : IssuerType.SELF_SIGNED);
@@ -1869,7 +1906,11 @@ export const certificateV3ServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Edit, ProjectPermissionCertificateActions.Edit,
ProjectPermissionSub.Certificates subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames ?? undefined,
serialNumber: certificate.serialNumber
})
); );
if (!certificate.profileId) { if (!certificate.profileId) {
@@ -1972,7 +2013,11 @@ export const certificateV3ServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Edit, ProjectPermissionCertificateActions.Edit,
ProjectPermissionSub.Certificates subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames ?? undefined,
serialNumber: certificate.serialNumber
})
); );
if (!certificate.profileId) { if (!certificate.profileId) {
@@ -4,6 +4,10 @@ import { TDbClient } from "@app/db";
import { TableName, TCertificates } from "@app/db/schemas"; import { TableName, TCertificates } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
import {
applyProcessedPermissionRulesToQuery,
type ProcessedPermissionRules
} from "@app/lib/knex/permission-filter-utils";
import { CertStatus } from "./certificate-types"; import { CertStatus } from "./certificate-types";
@@ -140,7 +144,8 @@ export const certificateDALFactory = (db: TDbClient) => {
const findActiveCertificatesForSync = async ( const findActiveCertificatesForSync = async (
filter: Partial<TCertificates & { friendlyName?: string; commonName?: string }>, filter: Partial<TCertificates & { friendlyName?: string; commonName?: string }>,
options?: { limit?: number; offset?: number } options?: { limit?: number; offset?: number },
permissionFilters?: ProcessedPermissionRules
): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => { ): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => {
try { try {
let query = db 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) { if (options?.offset) {
query = query.offset(options.offset); query = query.offset(options.offset);
} }
@@ -267,7 +276,8 @@ export const certificateDALFactory = (db: TDbClient) => {
const findWithPrivateKeyInfo = async ( const findWithPrivateKeyInfo = async (
filter: Partial<TCertificates & { friendlyName?: string; commonName?: string }>, filter: Partial<TCertificates & { friendlyName?: string; commonName?: string }>,
options?: { offset?: number; limit?: number; sort?: [string, "asc" | "desc"][] } options?: { offset?: number; limit?: number; sort?: [string, "asc" | "desc"][] },
permissionFilters?: ProcessedPermissionRules
): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => { ): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => {
try { try {
let query = db 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) { if (options?.offset) {
query = query.offset(options.offset); query = query.offset(options.offset);
} }
@@ -1,5 +1,5 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
@@ -108,7 +108,11 @@ export const certificateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Read, ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates subject(ProjectPermissionSub.Certificates, {
commonName: cert.commonName,
altNames: cert.altNames ?? undefined,
serialNumber: cert.serialNumber
})
); );
return { return {
@@ -140,7 +144,11 @@ export const certificateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.ReadPrivateKey, ProjectPermissionCertificateActions.ReadPrivateKey,
ProjectPermissionSub.Certificates subject(ProjectPermissionSub.Certificates, {
commonName: cert.commonName,
altNames: cert.altNames ?? undefined,
serialNumber: cert.serialNumber
})
); );
const { certPrivateKey } = await getCertificateCredentials({ const { certPrivateKey } = await getCertificateCredentials({
@@ -174,7 +182,11 @@ export const certificateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Delete, ProjectPermissionCertificateActions.Delete,
ProjectPermissionSub.Certificates subject(ProjectPermissionSub.Certificates, {
commonName: cert.commonName,
altNames: cert.altNames ?? undefined,
serialNumber: cert.serialNumber
})
); );
const deletedCert = await certificateDAL.deleteById(cert.id); const deletedCert = await certificateDAL.deleteById(cert.id);
@@ -234,7 +246,13 @@ export const certificateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Delete, 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"); if (cert.status === CertStatus.REVOKED) throw new Error("Certificate already revoked");
@@ -309,7 +327,11 @@ export const certificateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Read, 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 }); const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
@@ -397,7 +419,7 @@ export const certificateServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Create, ProjectPermissionCertificateActions.Import,
ProjectPermissionSub.Certificates ProjectPermissionSub.Certificates
); );
@@ -610,11 +632,23 @@ export const certificateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Read, 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( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.ReadPrivateKey, 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 }); const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
@@ -726,7 +760,13 @@ export const certificateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.ReadPrivateKey, 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) // Get certificate bundle (certificate, chain, private key)
+23 -4
View File
@@ -4,6 +4,10 @@ import { TDbClient } from "@app/db";
import { TableName, TPkiSyncs } from "@app/db/schemas"; import { TableName, TPkiSyncs } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex"; 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"; import { PkiSync } from "./pki-sync-enums";
@@ -45,13 +49,15 @@ const basePkiSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: PkiSyncF
const basePkiSyncWithSubscriberQuery = ({ const basePkiSyncWithSubscriberQuery = ({
filter, filter,
db, db,
tx tx,
processedRules
}: { }: {
db: TDbClient; db: TDbClient;
filter?: PkiSyncFindFilter; filter?: PkiSyncFindFilter;
tx?: Knex; 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.AppConnection, `${TableName.PkiSync}.connectionId`, `${TableName.AppConnection}.id`)
.leftJoin(TableName.PkiSubscriber, `${TableName.PkiSync}.subscriberId`, `${TableName.PkiSubscriber}.id`) .leftJoin(TableName.PkiSubscriber, `${TableName.PkiSync}.subscriberId`, `${TableName.PkiSubscriber}.id`)
.select(selectAllTableCols(TableName.PkiSync)) .select(selectAllTableCols(TableName.PkiSync))
@@ -82,6 +88,10 @@ const basePkiSyncWithSubscriberQuery = ({
void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.PkiSync, filter))); void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.PkiSync, filter)));
} }
if (processedRules) {
query = applyProcessedPermissionRulesToQuery(query, TableName.PkiSync, processedRules) as typeof query;
}
return 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 { try {
const pkiSyncs = await basePkiSyncWithSubscriberQuery({ filter: { projectId }, db, tx }); const pkiSyncs = await basePkiSyncWithSubscriberQuery({
filter: { projectId },
db,
tx,
processedRules
});
return pkiSyncs.map(expandPkiSyncWithSubscriber); return pkiSyncs.map(expandPkiSyncWithSubscriber);
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find By Project ID With Subscribers - PKI Sync" }); throw new DatabaseError({ error, name: "Find By Project ID With Subscribers - PKI Sync" });
@@ -4,6 +4,7 @@ import { ActionProjectType, TCertificateSyncs } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionPkiSyncActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; 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 { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
@@ -145,9 +146,10 @@ export const pkiSyncServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSyncActions.Create, ProjectPermissionPkiSyncActions.Create,
subscriber subject(ProjectPermissionSub.PkiSyncs, {
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: subscriber.name }) subscriberName: subscriber?.name,
: ProjectPermissionSub.PkiSyncs name
})
); );
// Get the destination app type based on PKI sync destination // Get the destination app type based on PKI sync destination
@@ -235,9 +237,10 @@ export const pkiSyncServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSyncActions.Edit, ProjectPermissionPkiSyncActions.Edit,
currentSubscriber subject(ProjectPermissionSub.PkiSyncs, {
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: currentSubscriber.name }) subscriberName: currentSubscriber?.name,
: ProjectPermissionSub.PkiSyncs name: pkiSync.name
})
); );
if (name && name !== pkiSync.name) { if (name && name !== pkiSync.name) {
@@ -331,9 +334,10 @@ export const pkiSyncServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSyncActions.Delete, ProjectPermissionPkiSyncActions.Delete,
pkiSyncSubscriber subject(ProjectPermissionSub.PkiSyncs, {
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: pkiSyncSubscriber.name }) subscriberName: pkiSyncSubscriber?.name,
: ProjectPermissionSub.PkiSyncs name: pkiSync.name
})
); );
return pkiSyncDAL.deleteById(id); return pkiSyncDAL.deleteById(id);
@@ -354,7 +358,13 @@ export const pkiSyncServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs); 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) { if (certificateId) {
const syncsWithCertificateInfo = await Promise.all( const syncsWithCertificateInfo = await Promise.all(
@@ -406,9 +416,10 @@ export const pkiSyncServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSyncActions.Read, ProjectPermissionPkiSyncActions.Read,
findSubscriber subject(ProjectPermissionSub.PkiSyncs, {
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: findSubscriber.name }) subscriberName: findSubscriber?.name,
: ProjectPermissionSub.PkiSyncs name: pkiSync.name
})
); );
const result = { const result = {
@@ -442,9 +453,10 @@ export const pkiSyncServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSyncActions.SyncCertificates, ProjectPermissionPkiSyncActions.SyncCertificates,
syncSubscriber subject(ProjectPermissionSub.PkiSyncs, {
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: syncSubscriber.name }) subscriberName: syncSubscriber?.name,
: ProjectPermissionSub.PkiSyncs name: pkiSync.name
})
); );
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: id }); await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: id });
@@ -483,9 +495,10 @@ export const pkiSyncServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSyncActions.ImportCertificates, ProjectPermissionPkiSyncActions.ImportCertificates,
importSubscriber subject(ProjectPermissionSub.PkiSyncs, {
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: importSubscriber.name }) subscriberName: importSubscriber?.name,
: ProjectPermissionSub.PkiSyncs name: pkiSync.name
})
); );
await pkiSyncQueue.queuePkiSyncImportCertificatesById({ syncId: id }); await pkiSyncQueue.queuePkiSyncImportCertificatesById({ syncId: id });
@@ -516,9 +529,10 @@ export const pkiSyncServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSyncActions.RemoveCertificates, ProjectPermissionPkiSyncActions.RemoveCertificates,
removeSubscriber subject(ProjectPermissionSub.PkiSyncs, {
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: removeSubscriber.name }) subscriberName: removeSubscriber?.name,
: ProjectPermissionSub.PkiSyncs name: pkiSync.name
})
); );
await pkiSyncQueue.queuePkiSyncRemoveCertificatesById({ syncId: id }); await pkiSyncQueue.queuePkiSyncRemoveCertificatesById({ syncId: id });
@@ -549,7 +563,18 @@ export const pkiSyncServiceFactory = ({
projectId: pkiSync.projectId 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); await validateCertificatesProjectOwnership(certificateIds, pkiSync.projectId);
@@ -588,7 +613,12 @@ export const pkiSyncServiceFactory = ({
projectId: pkiSync.projectId 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); const removedCount = await certificateSyncDAL.removeCertificates(pkiSyncId, certificateIds);
@@ -626,7 +656,12 @@ export const pkiSyncServiceFactory = ({
projectId: pkiSync.projectId 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({ const result = await certificateSyncDAL.findWithDetails({
pkiSyncId, pkiSyncId,
+28 -23
View File
@@ -20,6 +20,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCertificateAuthorityActions,
ProjectPermissionMemberActions, ProjectPermissionMemberActions,
ProjectPermissionPkiSubscriberActions, ProjectPermissionPkiSubscriberActions,
ProjectPermissionPkiTemplateActions, 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 { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal"; import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal";
import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { DatabaseErrorCode } from "@app/lib/error-codes"; import { DatabaseErrorCode } from "@app/lib/error-codes";
@@ -911,7 +913,7 @@ export const projectServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionCertificateAuthorityActions.Read,
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateAuthorities
); );
@@ -963,35 +965,38 @@ export const projectServiceFactory = ({
ProjectPermissionSub.Certificates ProjectPermissionSub.Certificates
); );
const regularFilters = {
projectId,
...(friendlyName && { friendlyName }),
...(commonName && { commonName })
};
const permissionFilters = getProcessedPermissionRules(
permission,
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
const certificates = forPkiSync const certificates = forPkiSync
? await certificateDAL.findActiveCertificatesForSync( ? await certificateDAL.findActiveCertificatesForSync(regularFilters, { offset, limit }, permissionFilters)
{
projectId,
...(friendlyName && { friendlyName }),
...(commonName && { commonName })
},
{ offset, limit }
)
: await certificateDAL.findWithPrivateKeyInfo( : await certificateDAL.findWithPrivateKeyInfo(
regularFilters,
{ {
projectId, offset,
...(friendlyName && { friendlyName }), limit,
...(commonName && { commonName }) 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 const count = forPkiSync
? await certificateDAL.countActiveCertificatesForSync({ ? await certificateDAL.countActiveCertificatesForSync(countFilter)
projectId, : await certificateDAL.countCertificatesInProject(countFilter);
friendlyName,
commonName
})
: await certificateDAL.countCertificatesInProject({
projectId,
friendlyName,
commonName
});
return { return {
certificates, certificates,
-2
View File
@@ -2493,8 +2493,6 @@
"pages": [ "pages": [
"api-reference/endpoints/certificates/list", "api-reference/endpoints/certificates/list",
"api-reference/endpoints/certificates/read", "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/renew",
"api-reference/endpoints/certificates/update-config", "api-reference/endpoints/certificates/update-config",
"api-reference/endpoints/certificates/revoke", "api-reference/endpoints/certificates/revoke",
@@ -4,6 +4,7 @@ export {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionAuditLogsActions, ProjectPermissionAuditLogsActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCertificateAuthorityActions,
ProjectPermissionCertificateProfileActions, ProjectPermissionCertificateProfileActions,
ProjectPermissionCmekActions, ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
@@ -12,6 +13,7 @@ export {
ProjectPermissionKmipActions, ProjectPermissionKmipActions,
ProjectPermissionMemberActions, ProjectPermissionMemberActions,
ProjectPermissionPkiSubscriberActions, ProjectPermissionPkiSubscriberActions,
ProjectPermissionPkiSyncActions,
ProjectPermissionPkiTemplateActions, ProjectPermissionPkiTemplateActions,
ProjectPermissionSshHostActions, ProjectPermissionSshHostActions,
ProjectPermissionSub ProjectPermissionSub
@@ -12,7 +12,9 @@ export enum ProjectPermissionCertificateActions {
Create = "create", Create = "create",
Edit = "edit", Edit = "edit",
Delete = "delete", Delete = "delete",
ReadPrivateKey = "read-private-key" List = "list",
ReadPrivateKey = "read-private-key",
Import = "import"
} }
export enum ProjectPermissionSecretActions { export enum ProjectPermissionSecretActions {
@@ -67,6 +69,7 @@ export enum ProjectPermissionPkiSyncActions {
Create = "create", Create = "create",
Edit = "edit", Edit = "edit",
Delete = "delete", Delete = "delete",
List = "list",
SyncCertificates = "sync-certificates", SyncCertificates = "sync-certificates",
ImportCertificates = "import-certificates", ImportCertificates = "import-certificates",
RemoveCertificates = "remove-certificates" RemoveCertificates = "remove-certificates"
@@ -128,13 +131,25 @@ export enum ProjectPermissionPkiTemplateActions {
ListCerts = "list-certs" ListCerts = "list-certs"
} }
export enum ProjectPermissionCertificateProfileActions { export enum ProjectPermissionCertificateAuthorityActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
Edit = "edit", Edit = "edit",
Delete = "delete", 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", IssueCert = "issue-cert",
RevealAcmeEabSecret = "reveal-acme-eab-secret" RevealAcmeEabSecret = "reveal-acme-eab-secret",
RotateAcmeEabSecret = "rotate-acme-eab-secret"
} }
export enum ProjectPermissionSecretRotationActions { export enum ProjectPermissionSecretRotationActions {
@@ -230,6 +245,9 @@ export type ConditionalProjectPermissionSubject =
| ProjectPermissionSub.SshHosts | ProjectPermissionSub.SshHosts
| ProjectPermissionSub.PkiSubscribers | ProjectPermissionSub.PkiSubscribers
| ProjectPermissionSub.CertificateTemplates | ProjectPermissionSub.CertificateTemplates
| ProjectPermissionSub.CertificateAuthorities
| ProjectPermissionSub.Certificates
| ProjectPermissionSub.CertificateProfiles
| ProjectPermissionSub.SecretFolders | ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports | ProjectPermissionSub.SecretImports
| ProjectPermissionSub.SecretRotation | ProjectPermissionSub.SecretRotation
@@ -361,7 +379,22 @@ export type SecretSyncSubjectFields = {
}; };
export type PkiSyncSubjectFields = { 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 = { export type SecretRotationSubjectFields = {
@@ -457,8 +490,21 @@ export type ProjectPermissionSet =
| (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields) | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
) )
] ]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates] ProjectPermissionCertificateAuthorityActions,
(
| ProjectPermissionSub.CertificateAuthorities
| (ForcedSubject<ProjectPermissionSub.CertificateAuthorities> &
CertificateAuthoritySubjectFields)
)
]
| [
ProjectPermissionCertificateActions,
(
| ProjectPermissionSub.Certificates
| (ForcedSubject<ProjectPermissionSub.Certificates> & CertificateSubjectFields)
)
]
| [ | [
ProjectPermissionPkiTemplateActions, ProjectPermissionPkiTemplateActions,
( (
@@ -484,7 +530,14 @@ export type ProjectPermissionSet =
| (ForcedSubject<ProjectPermissionSub.PkiSubscribers> & PkiSubscriberSubjectFields) | (ForcedSubject<ProjectPermissionSub.PkiSubscribers> & PkiSubscriberSubjectFields)
) )
] ]
| [ProjectPermissionCertificateProfileActions, ProjectPermissionSub.CertificateProfiles] | [
ProjectPermissionCertificateProfileActions,
(
| ProjectPermissionSub.CertificateProfiles
| (ForcedSubject<ProjectPermissionSub.CertificateProfiles> &
CertificateProfileSubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
+2
View File
@@ -15,6 +15,7 @@ export {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionAuditLogsActions, ProjectPermissionAuditLogsActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCertificateAuthorityActions,
ProjectPermissionCertificateProfileActions, ProjectPermissionCertificateProfileActions,
ProjectPermissionCmekActions, ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
@@ -23,6 +24,7 @@ export {
ProjectPermissionKmipActions, ProjectPermissionKmipActions,
ProjectPermissionMemberActions, ProjectPermissionMemberActions,
ProjectPermissionPkiSubscriberActions, ProjectPermissionPkiSubscriberActions,
ProjectPermissionPkiSyncActions,
ProjectPermissionPkiTemplateActions, ProjectPermissionPkiTemplateActions,
ProjectPermissionSshHostActions, ProjectPermissionSshHostActions,
ProjectPermissionSub, ProjectPermissionSub,
@@ -72,7 +72,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.SIGN_INTERMEDIATE]: "Sign intermediate", [EventType.SIGN_INTERMEDIATE]: "Sign intermediate",
[EventType.IMPORT_CA_CERT]: "Import CA certificate", [EventType.IMPORT_CA_CERT]: "Import CA certificate",
[EventType.GET_CA_CRL]: "Get CA CRL", [EventType.GET_CA_CRL]: "Get CA CRL",
[EventType.ISSUE_CERT]: "Issue certificate", [EventType.ISSUE_CERT]: "Request certificate",
[EventType.IMPORT_CERT]: "Import certificate", [EventType.IMPORT_CERT]: "Import certificate",
[EventType.GET_CERT]: "Get certificate", [EventType.GET_CERT]: "Get certificate",
[EventType.DELETE_CERT]: "Delete 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.UPDATE_PKI_SUBSCRIBER]: "Update PKI subscriber",
[EventType.DELETE_PKI_SUBSCRIBER]: "Delete PKI subscriber", [EventType.DELETE_PKI_SUBSCRIBER]: "Delete PKI subscriber",
[EventType.GET_PKI_SUBSCRIBER]: "Get 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.SIGN_PKI_SUBSCRIBER_CERT]: "Sign PKI subscriber certificate",
[EventType.AUTOMATED_RENEW_SUBSCRIBER_CERT]: "Automated renew PKI subscriber certificate", [EventType.AUTOMATED_RENEW_SUBSCRIBER_CERT]: "Automated renew PKI subscriber certificate",
[EventType.LIST_PKI_SUBSCRIBER_CERTS]: "List PKI subscriber certificates", [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.DELETE_CERTIFICATE_PROFILE]: "Delete Certificate Profile",
[EventType.GET_CERTIFICATE_PROFILE]: "Get Certificate Profile", [EventType.GET_CERTIFICATE_PROFILE]: "Get Certificate Profile",
[EventType.LIST_CERTIFICATE_PROFILES]: "List Certificate Profiles", [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.SIGN_CERTIFICATE_FROM_PROFILE]: "Sign Certificate From Profile",
[EventType.ORDER_CERTIFICATE_FROM_PROFILE]: "Order Certificate From Profile", [EventType.ORDER_CERTIFICATE_FROM_PROFILE]: "Order Certificate From Profile",
[EventType.GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE]: [EventType.GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE]:
@@ -1,4 +1,5 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { subject } from "@casl/ability";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons"; import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router"; import { Link, useNavigate, useParams } from "@tanstack/react-router";
@@ -7,6 +8,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
AccessRestrictedBanner,
Button, Button,
DeleteActionModal, DeleteActionModal,
DropdownMenu, DropdownMenu,
@@ -18,7 +20,7 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { import {
ProjectPermissionActions, ProjectPermissionCertificateAuthorityActions,
ProjectPermissionSub, ProjectPermissionSub,
useOrganization, useOrganization,
useProject useProject
@@ -88,63 +90,80 @@ const Page = () => {
return ( return (
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-8xl"> <ProjectPermissionCan
<Link I={ProjectPermissionCertificateAuthorityActions.Read}
to="/organizations/$orgId/projects/cert-management/$projectId/certificate-authorities" a={subject(ProjectPermissionSub.CertificateAuthorities, {
params={{ name: data.name
orgId: currentOrg.id, })}
projectId >
}} {(isAllowed) =>
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400" isAllowed ? (
> <div className="mx-auto mb-6 w-full max-w-8xl">
<FontAwesomeIcon icon={faChevronLeft} /> <Link
Certificate Authorities to="/organizations/$orgId/projects/cert-management/$projectId/certificate-authorities"
</Link> params={{
<PageHeader orgId: currentOrg.id,
scope={ProjectType.CertificateManager} projectId
description="Manage certificate authority" }}
title={data.name} className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<Button variant="outline_bg">More</Button>
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.CertificateAuthorities}
> >
{(isAllowed) => ( <FontAwesomeIcon icon={faChevronLeft} />
<DropdownMenuItem Certificate Authorities
className={twMerge( </Link>
isAllowed <PageHeader
? "hover:bg-red-500! hover:text-white!" scope={ProjectType.CertificateManager}
: "pointer-events-none cursor-not-allowed opacity-50" description="Manage certificate authority"
)} title={data.name}
onClick={() => handlePopUpOpen("deleteCa")} >
disabled={!isAllowed} <DropdownMenu>
> <DropdownMenuTrigger asChild className="rounded-lg">
Delete CA <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
</DropdownMenuItem> <Tooltip content="More options">
)} <Button variant="outline_bg">More</Button>
</ProjectPermissionCan> </Tooltip>
</DropdownMenuContent> </div>
</DropdownMenu> </DropdownMenuTrigger>
</PageHeader> <DropdownMenuContent align="end" className="p-1">
<div className="flex"> <ProjectPermissionCan
<div className="mr-4 w-96"> I={ProjectPermissionCertificateAuthorityActions.Delete}
<CaDetailsSection caId={data.id} handlePopUpOpen={handlePopUpOpen} /> a={subject(ProjectPermissionSub.CertificateAuthorities, {
</div> name: data.name
<div className="w-full"> })}
<CaCertificatesSection caId={data.id} /> >
<CaCrlsSection caId={data.id} /> {(canDelete) => (
</div> <DropdownMenuItem
</div> className={twMerge(
</div> canDelete
? "hover:bg-red-500! hover:text-white!"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => handlePopUpOpen("deleteCa")}
disabled={!canDelete}
>
Delete CA
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</PageHeader>
<div className="flex">
<div className="mr-4 w-96">
<CaDetailsSection caId={data.id} handlePopUpOpen={handlePopUpOpen} />
</div>
<div className="w-full">
<CaCertificatesSection caId={data.id} caName={data.name} />
<CaCrlsSection caId={data.id} />
</div>
</div>
</div>
) : (
<div className="container mx-auto flex h-full items-center justify-center">
<AccessRestrictedBanner />
</div>
)
}
</ProjectPermissionCan>
)} )}
<CaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <CaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <CaRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
@@ -2,9 +2,10 @@ import { CaCertificatesTable } from "./CaCertificatesTable";
type Props = { type Props = {
caId: string; caId: string;
caName: string;
}; };
export const CaCertificatesSection = ({ caId }: Props) => { export const CaCertificatesSection = ({ caId, caName }: Props) => {
return ( return (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
@@ -21,7 +22,7 @@ export const CaCertificatesSection = ({ caId }: Props) => {
</IconButton> */} </IconButton> */}
</div> </div>
<div className="py-4"> <div className="py-4">
<CaCertificatesTable caId={caId} /> <CaCertificatesTable caId={caId} caName={caName} />
</div> </div>
</div> </div>
); );
@@ -1,3 +1,4 @@
import { subject } from "@casl/ability";
import { faCertificate, faEllipsis } from "@fortawesome/free-solid-svg-icons"; import { faCertificate, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
@@ -22,14 +23,15 @@ import {
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge } from "@app/components/v3"; import { Badge } from "@app/components/v3";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionCertificateAuthorityActions, ProjectPermissionSub } from "@app/context";
import { useGetCaCerts } from "@app/hooks/api"; import { useGetCaCerts } from "@app/hooks/api";
type Props = { type Props = {
caId: string; caId: string;
caName: string;
}; };
export const CaCertificatesTable = ({ caId }: Props) => { export const CaCertificatesTable = ({ caId, caName }: Props) => {
const { data: caCerts, isPending } = useGetCaCerts(caId); const { data: caCerts, isPending } = useGetCaCerts(caId);
const downloadTxtFile = (filename: string, content: string) => { const downloadTxtFile = (filename: string, content: string) => {
@@ -77,8 +79,10 @@ export const CaCertificatesTable = ({ caId }: Props) => {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionCertificateAuthorityActions.Read}
a={ProjectPermissionSub.Identity} a={subject(ProjectPermissionSub.CertificateAuthorities, {
name: caName
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -96,8 +100,10 @@ export const CaCertificatesTable = ({ caId }: Props) => {
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionCertificateAuthorityActions.Read}
a={ProjectPermissionSub.Identity} a={subject(ProjectPermissionSub.CertificateAuthorities, {
name: caName
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -1,10 +1,11 @@
import { subject } from "@casl/ability";
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns"; import { format } from "date-fns";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, IconButton, Tooltip } from "@app/components/v2"; import { Button, IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionCertificateAuthorityActions, ProjectPermissionSub } from "@app/context";
import { useTimedReset } from "@app/hooks"; import { useTimedReset } from "@app/hooks";
import { CaStatus, CaType, InternalCaType, useGetCa } from "@app/hooks/api"; import { CaStatus, CaType, InternalCaType, useGetCa } from "@app/hooks/api";
import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants"; import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants";
@@ -39,7 +40,10 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-medium text-mineshaft-100">CA Details</h3> <h3 className="text-lg font-medium text-mineshaft-100">CA Details</h3>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Identity}> <ProjectPermissionCan
I={ProjectPermissionCertificateAuthorityActions.Edit}
a={subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })}
>
{(isAllowed) => { {(isAllowed) => {
return ( return (
<Tooltip content="Edit CA"> <Tooltip content="Edit CA">
@@ -154,8 +158,8 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
</div> </div>
{ca.status === CaStatus.ACTIVE && ( {ca.status === CaStatus.ACTIVE && (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionCertificateAuthorityActions.Renew}
a={ProjectPermissionSub.CertificateAuthorities} a={subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })}
> >
{(isAllowed) => { {(isAllowed) => {
return ( return (
@@ -190,8 +194,8 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
)} )}
{ca.status === CaStatus.PENDING_CERTIFICATE && ( {ca.status === CaStatus.PENDING_CERTIFICATE && (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionCertificateAuthorityActions.Create}
a={ProjectPermissionSub.CertificateAuthorities} a={subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })}
> >
{(isAllowed) => { {(isAllowed) => {
return ( return (
@@ -4,7 +4,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2"; 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 { CaStatus, CaType, useDeleteCa, useUpdateCa } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
@@ -57,7 +61,7 @@ export const CaSection = () => {
<div className="mb-4 flex justify-between"> <div className="mb-4 flex justify-between">
<p className="text-xl font-medium text-mineshaft-100">Internal Certificate Authorities</p> <p className="text-xl font-medium text-mineshaft-100">Internal Certificate Authorities</p>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionCertificateAuthorityActions.Create}
a={ProjectPermissionSub.CertificateAuthorities} a={ProjectPermissionSub.CertificateAuthorities}
> >
{(isAllowed) => ( {(isAllowed) => (
@@ -100,6 +104,7 @@ export const CaSection = () => {
: "This action will prevent the CA from issuing new certificates." : "This action will prevent the CA from issuing new certificates."
} }
onChange={(isOpen) => handlePopUpToggle("caStatus", isOpen)} onChange={(isOpen) => handlePopUpToggle("caStatus", isOpen)}
buttonText="Confirm"
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={() => onDeleteApproved={() =>
onUpdateCaStatus(popUp?.caStatus?.data as { caId: string; status: CaStatus }) onUpdateCaStatus(popUp?.caStatus?.data as { caId: string; status: CaStatus })
@@ -1,3 +1,4 @@
import { subject } from "@casl/ability";
import { faBan, faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons"; import { faBan, faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
@@ -23,10 +24,11 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge } from "@app/components/v3"; import { Badge } from "@app/components/v3";
import { import {
ProjectPermissionActions, ProjectPermissionCertificateAuthorityActions,
ProjectPermissionSub, ProjectPermissionSub,
useOrganization, useOrganization,
useProject useProject,
useProjectPermission
} from "@app/context"; } from "@app/context";
import { CaStatus, CaType, useListCasByTypeAndProjectId } from "@app/hooks/api"; import { CaStatus, CaType, useListCasByTypeAndProjectId } from "@app/hooks/api";
import { import {
@@ -53,6 +55,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { currentProject } = useProject(); const { currentProject } = useProject();
const { permission } = useProjectPermission();
const { data, isPending } = useListCasByTypeAndProjectId(CaType.INTERNAL, currentProject.id); const { data, isPending } = useListCasByTypeAndProjectId(CaType.INTERNAL, currentProject.id);
const cas = data as TInternalCertificateAuthority[]; const cas = data as TInternalCertificateAuthority[];
@@ -75,11 +78,23 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
cas && cas &&
cas.length > 0 && cas.length > 0 &&
cas.map((ca) => { cas.map((ca) => {
const canReadCa = permission.can(
ProjectPermissionCertificateAuthorityActions.Read,
subject(ProjectPermissionSub.CertificateAuthorities, {
name: ca.name
})
);
return ( return (
<Tr <Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700" className={twMerge(
"h-10 transition-colors duration-100",
canReadCa && "cursor-pointer hover:bg-mineshaft-700",
!canReadCa && "cursor-not-allowed opacity-60"
)}
key={`ca-${ca.id}`} key={`ca-${ca.id}`}
onClick={() => onClick={() =>
canReadCa &&
navigate({ navigate({
to: "/organizations/$orgId/projects/cert-management/$projectId/ca/$caId", to: "/organizations/$orgId/projects/cert-management/$projectId/ca/$caId",
params: { params: {
@@ -118,8 +133,10 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
{ca.status === CaStatus.PENDING_CERTIFICATE && ( {ca.status === CaStatus.PENDING_CERTIFICATE && (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionCertificateAuthorityActions.Create}
a={ProjectPermissionSub.CertificateAuthorities} a={subject(ProjectPermissionSub.CertificateAuthorities, {
name: ca.name
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -143,8 +160,10 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
)} )}
{ca.status !== CaStatus.PENDING_CERTIFICATE && ( {ca.status !== CaStatus.PENDING_CERTIFICATE && (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Read} I={ProjectPermissionCertificateAuthorityActions.Read}
a={ProjectPermissionSub.CertificateAuthorities} a={subject(ProjectPermissionSub.CertificateAuthorities, {
name: ca.name
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -168,8 +187,10 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
)} )}
{(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && ( {(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionCertificateAuthorityActions.Edit}
a={ProjectPermissionSub.CertificateAuthorities} a={subject(ProjectPermissionSub.CertificateAuthorities, {
name: ca.name
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -196,8 +217,10 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
</ProjectPermissionCan> </ProjectPermissionCan>
)} )}
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionCertificateAuthorityActions.Delete}
a={ProjectPermissionSub.CertificateAuthorities} a={subject(ProjectPermissionSub.CertificateAuthorities, {
name: ca.name
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -4,7 +4,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2"; 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 { CaStatus, CaType, useDeleteCa, useUpdateCa } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
@@ -61,7 +65,7 @@ export const ExternalCaSection = () => {
<div className="mb-4 flex justify-between"> <div className="mb-4 flex justify-between">
<p className="text-xl font-medium text-mineshaft-100">External Certificate Authorities</p> <p className="text-xl font-medium text-mineshaft-100">External Certificate Authorities</p>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionCertificateAuthorityActions.Create}
a={ProjectPermissionSub.CertificateAuthorities} a={ProjectPermissionSub.CertificateAuthorities}
> >
{(isAllowed) => ( {(isAllowed) => (
@@ -1,3 +1,4 @@
import { subject } from "@casl/ability";
import { import {
faBan, faBan,
faCertificate, faCertificate,
@@ -26,7 +27,12 @@ import {
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge } from "@app/components/v3"; 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 { CaStatus, CaType, useListExternalCasByProjectId } from "@app/hooks/api";
import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants"; import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -45,6 +51,7 @@ type Props = {
export const ExternalCaTable = ({ handlePopUpOpen }: Props) => { export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
const { currentProject } = useProject(); const { currentProject } = useProject();
const { permission } = useProjectPermission();
const { data, isPending } = useListExternalCasByProjectId(currentProject.id); const { data, isPending } = useListExternalCasByProjectId(currentProject.id);
return ( return (
@@ -65,17 +72,29 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
data && data &&
data.length > 0 && data.length > 0 &&
data.map((ca) => { data.map((ca) => {
const canEditCa = permission.can(
ProjectPermissionCertificateAuthorityActions.Edit,
subject(ProjectPermissionSub.CertificateAuthorities, {
name: ca.name
})
);
return ( return (
<Tr <Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700" className={twMerge(
"h-10 transition-colors duration-100",
canEditCa && "cursor-pointer hover:bg-mineshaft-700",
!canEditCa && "cursor-not-allowed opacity-60"
)}
key={`ca-${ca.id}`} key={`ca-${ca.id}`}
onClick={() => { onClick={() =>
canEditCa &&
handlePopUpOpen("ca", { handlePopUpOpen("ca", {
caId: ca.id, caId: ca.id,
name: ca.name, name: ca.name,
type: ca.type type: ca.type
}); })
}} }
> >
<Td>{ca.name}</Td> <Td>{ca.name}</Td>
<Td>{ca.type}</Td> <Td>{ca.type}</Td>
@@ -95,8 +114,10 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionCertificateAuthorityActions.Edit}
a={ProjectPermissionSub.CertificateAuthorities} a={subject(ProjectPermissionSub.CertificateAuthorities, {
name: ca.name
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -120,8 +141,10 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
</ProjectPermissionCan> </ProjectPermissionCan>
{(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && ( {(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionCertificateAuthorityActions.Edit}
a={ProjectPermissionSub.CertificateAuthorities} a={subject(ProjectPermissionSub.CertificateAuthorities, {
name: ca.name
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -149,8 +172,10 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
</ProjectPermissionCan> </ProjectPermissionCan>
)} )}
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionCertificateAuthorityActions.Delete}
a={ProjectPermissionSub.CertificateAuthorities} a={subject(ProjectPermissionSub.CertificateAuthorities, {
name: ca.name
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -241,7 +241,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle, preselectedTemplate
setCertificateDetails(null); setCertificateDetails(null);
}} }}
> >
<ModalContent title={`${cert ? "View" : "Issue"} Certificate`}> <ModalContent title={`${cert ? "View" : "Request"} Certificate`}>
{!certificateDetails ? ( {!certificateDetails ? (
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onFormSubmit)}>
<Controller <Controller
@@ -6,6 +6,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2"; import { Button, DeleteActionModal } from "@app/components/v2";
import { import {
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCertificateProfileActions,
ProjectPermissionSub, ProjectPermissionSub,
useProject useProject
} from "@app/context"; } from "@app/context";
@@ -104,12 +105,12 @@ export const CertificatesSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between"> <div className="mb-4 flex justify-between">
<p className="text-xl font-medium text-mineshaft-100">Certificates</p> <p className="text-xl font-medium text-mineshaft-100">Certificates</p>
<ProjectPermissionCan <div className="flex gap-2">
I={ProjectPermissionCertificateActions.Create} <ProjectPermissionCan
a={ProjectPermissionSub.Certificates} I={ProjectPermissionCertificateActions.Import}
> a={ProjectPermissionSub.Certificates}
{(isAllowed) => ( >
<div className="flex gap-2"> {(isAllowed) => (
<Button <Button
variant="outline_bg" variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faArrowRight} />} leftIcon={<FontAwesomeIcon icon={faArrowRight} />}
@@ -118,6 +119,13 @@ export const CertificatesSection = () => {
> >
Import Import
</Button> </Button>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionCertificateProfileActions.IssueCert}
a={ProjectPermissionSub.CertificateProfiles}
>
{(isAllowed) => (
<Button <Button
colorSchema="primary" colorSchema="primary"
type="submit" type="submit"
@@ -127,9 +135,9 @@ export const CertificatesSection = () => {
> >
Request Request
</Button> </Button>
</div> )}
)} </ProjectPermissionCan>
</ProjectPermissionCan> </div>
</div> </div>
<CertificatesTable handlePopUpOpen={handlePopUpOpen} /> <CertificatesTable handlePopUpOpen={handlePopUpOpen} />
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
@@ -1,4 +1,5 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { subject } from "@casl/ability";
import { import {
faBan, faBan,
faCertificate, faCertificate,
@@ -40,8 +41,10 @@ import {
import { Badge } from "@app/components/v3"; import { Badge } from "@app/components/v3";
import { import {
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionPkiSyncActions,
ProjectPermissionSub, ProjectPermissionSub,
useProject useProject,
useProjectPermission
} from "@app/context"; } from "@app/context";
import { useUpdateRenewalConfig } from "@app/hooks/api"; import { useUpdateRenewalConfig } from "@app/hooks/api";
import { caSupportsCapability } from "@app/hooks/api/ca/constants"; 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 [perPage, setPerPage] = useState(PER_PAGE_INIT);
const { currentProject } = useProject(); const { currentProject } = useProject();
const { permission } = useProjectPermission();
const { data, isPending } = useListWorkspaceCertificates({ const { data, isPending } = useListWorkspaceCertificates({
projectId: currentProject?.id ?? "", projectId: currentProject?.id ?? "",
offset: (page - 1) * perPage, offset: (page - 1) * perPage,
@@ -207,55 +211,72 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
return "opacity-0 group-hover:opacity-100"; return "opacity-0 group-hover:opacity-100";
})()}`} })()}`}
> >
{canShowAutoRenewalIcon && ( {canShowAutoRenewalIcon &&
<Tooltip (() => {
content={(() => { const canEditCertificate = permission.can(
if (hasFailed && certificate.renewalError) { ProjectPermissionCertificateActions.Edit,
return `Auto-renewal failed: ${certificate.renewalError}`; subject(ProjectPermissionSub.Certificates, {
} commonName: certificate.commonName,
if (isAutoRenewalEnabled) { altNames: certificate.altNames,
const expiryDate = new Date(certificate.notAfter); serialNumber: certificate.serialNumber
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";
})()}
>
<button
type="button"
className={(() => {
if (hasFailed) return "pr-1 text-red-500 hover:text-red-400";
return "pr-1 text-primary-500 hover:text-primary-400";
})()}
aria-label="Certificate auto-renewal"
onClick={(e) => {
e.stopPropagation();
if (hasFailed) return;
handlePopUpOpen("manageRenewal", { return (
certificateId: certificate.id, <Tooltip
commonName: originalDisplayName, content={(() => {
profileId: certificate.profileId || "", if (hasFailed && certificate.renewalError) {
renewBeforeDays: certificate.renewBeforeDays || 7, return `Auto-renewal failed: ${certificate.renewalError}`;
ttlDays: Math.ceil( }
(new Date(certificate.notAfter).getTime() - if (isAutoRenewalEnabled) {
new Date(certificate.notBefore).getTime()) / const expiryDate = new Date(certificate.notAfter);
(24 * 60 * 60 * 1000) const now = new Date();
) const daysUntilExpiry = Math.ceil(
}); (expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)
}} );
> const daysUntilRenewal = Math.max(
<FontAwesomeIcon icon={faClockRotateLeft} /> 0,
</button> daysUntilExpiry - (certificate.renewBeforeDays || 0)
</Tooltip> );
)} return `Auto-renews in ${daysUntilRenewal}d`;
}
return "Set auto renewal";
})()}
>
<button
type="button"
className={(() => {
if (hasFailed) return "pr-1 text-red-500 hover:text-red-400";
return "pr-1 text-primary-500 hover:text-primary-400";
})()}
aria-label="Certificate auto-renewal"
onClick={(e) => {
e.stopPropagation();
if (!canEditCertificate) return;
if (hasFailed) return;
handlePopUpOpen("manageRenewal", {
certificateId: certificate.id,
commonName: originalDisplayName,
profileId: certificate.profileId || "",
renewBeforeDays: certificate.renewBeforeDays || 7,
ttlDays: Math.ceil(
(new Date(certificate.notAfter).getTime() -
new Date(certificate.notBefore).getTime()) /
(24 * 60 * 60 * 1000)
),
notAfter: certificate.notAfter,
renewalError: certificate.renewalError,
renewedFromCertificateId: certificate.renewedFromCertificateId,
renewedByCertificateId: certificate.renewedByCertificateId
});
}}
>
<FontAwesomeIcon icon={faClockRotateLeft} />
</button>
</Tooltip>
);
})()}
</div> </div>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
@@ -268,7 +289,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionCertificateActions.Read} I={ProjectPermissionCertificateActions.Read}
a={ProjectPermissionSub.Certificates} a={subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames,
serialNumber: certificate.serialNumber,
friendlyName: certificate.friendlyName
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -291,7 +317,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
{isLegacyTemplatesEnabled && ( {isLegacyTemplatesEnabled && (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionCertificateActions.Read} I={ProjectPermissionCertificateActions.Read}
a={ProjectPermissionSub.Certificates} a={subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames,
serialNumber: certificate.serialNumber,
friendlyName: certificate.friendlyName
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -327,7 +358,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
return ( return (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionCertificateActions.Edit} I={ProjectPermissionCertificateActions.Edit}
a={ProjectPermissionSub.Certificates} a={subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames,
serialNumber: certificate.serialNumber,
friendlyName: certificate.friendlyName
})}
> >
{(isAllowed) => { {(isAllowed) => {
return ( return (
@@ -391,7 +427,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
return ( return (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionCertificateActions.Edit} I={ProjectPermissionCertificateActions.Edit}
a={ProjectPermissionSub.Certificates} a={subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames,
serialNumber: certificate.serialNumber,
friendlyName: certificate.friendlyName
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -428,7 +469,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
return ( return (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionCertificateActions.Edit} I={ProjectPermissionCertificateActions.Edit}
a={ProjectPermissionSub.Certificates} a={subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames,
serialNumber: certificate.serialNumber,
friendlyName: certificate.friendlyName
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -455,8 +501,8 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
{certificate.status === CertStatus.ACTIVE && {certificate.status === CertStatus.ACTIVE &&
!certificate.renewedByCertificateId && ( !certificate.renewedByCertificateId && (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionCertificateActions.Edit} I={ProjectPermissionPkiSyncActions.Edit}
a={ProjectPermissionSub.Certificates} a={ProjectPermissionSub.PkiSyncs}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -478,21 +524,26 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
)} )}
{/* 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 caType = caCapabilityMap[certificate.caId];
const supportsRevocation = const supportsRevocation =
!caType || !caType ||
caSupportsCapability(caType, CaCapability.REVOKE_CERTIFICATES); caSupportsCapability(caType, CaCapability.REVOKE_CERTIFICATES);
if (!supportsRevocation) { if (!supportsRevocation || isRevoked) {
return null; return null;
} }
return ( return (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionCertificateActions.Delete} I={ProjectPermissionCertificateActions.Delete}
a={ProjectPermissionSub.Certificates} a={subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames,
serialNumber: certificate.serialNumber,
friendlyName: certificate.friendlyName
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -516,7 +567,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
})()} })()}
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionCertificateActions.Delete} I={ProjectPermissionCertificateActions.Delete}
a={ProjectPermissionSub.Certificates} a={subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames,
serialNumber: certificate.serialNumber,
friendlyName: certificate.friendlyName
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
@@ -67,7 +67,6 @@ export const PkiSyncRow = ({
const navigate = useNavigate(); const navigate = useNavigate();
const { const {
id, id,
subscriberId,
lastSyncMessage, lastSyncMessage,
destination, destination,
lastSyncedAt, lastSyncedAt,
@@ -117,263 +116,286 @@ export const PkiSyncRow = ({
const destinationDetails = PKI_SYNC_MAP[destination]; const destinationDetails = PKI_SYNC_MAP[destination];
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: subscriberId || "" subscriberName: destinationName,
name
}); });
return ( return (
<Tr <ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Read} a={permissionSubject}>
onClick={() => { {(isAllowed: boolean) => {
// console.log("PKI Sync navigation:", { syncId: id, projectId }); return (
navigate({ <Tr
to: ROUTE_PATHS.CertManager.PkiSyncDetailsByIDPage.path, onClick={() => {
params: { if (!isAllowed) {
syncId: id, return;
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}`}
>
<Td>
<img
alt={`${destinationDetails.name} sync`}
src={`/images/integrations/${destinationDetails.image}`}
className="min-w-7"
/>
</Td>
<Td className="max-w-0 min-w-32!">
<div>
<div className="flex w-full items-center">
<p className="truncate">{name}</p>
{description && (
<Tooltip content={description}>
<FontAwesomeIcon
icon={faInfoCircle}
size="xs"
className="ml-1 text-mineshaft-400"
/>
</Tooltip>
)}
</div>
<p className="truncate text-xs leading-4 text-bunker-300">{destinationDetails.name}</p>
</div>
</Td>
<PkiSyncDestinationCol pkiSync={pkiSync} />
<Td>
<div className="flex items-center gap-1">
{syncStatus && (
<Tooltip
position="left"
className="max-w-sm"
content={
[PkiSyncStatus.Succeeded, PkiSyncStatus.Failed].includes(syncStatus) ? (
<div className="flex flex-col gap-2 py-1 whitespace-normal">
{lastSyncedAt && (
<div>
<div
className={`mb-2 flex self-start ${syncStatus === PkiSyncStatus.Failed ? "text-yellow" : "text-green"}`}
>
<FontAwesomeIcon
icon={faCalendarCheck}
className="ml-1 pt-0.5 pr-1.5 text-sm"
/>
<div className="text-xs">Last Synced</div>
</div>
<div className="rounded-sm bg-mineshaft-600 p-2 text-xs">
{format(new Date(lastSyncedAt), "yyyy-MM-dd, hh:mm aaa")}
</div>
</div>
)}
{failureMessage && (
<div>
<div className="mb-2 flex self-start text-red">
<FontAwesomeIcon icon={faXmark} className="ml-1 pt-0.5 pr-1.5 text-sm" />
<div className="text-xs">Failure Reason</div>
</div>
<div className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">
{failureMessage}
</div>
</div>
)}
</div>
) : undefined
} }
> // 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}`}
>
<Td>
<img
alt={`${destinationDetails.name} sync`}
src={`/images/integrations/${destinationDetails.image}`}
className="min-w-7"
/>
</Td>
<Td className="max-w-0 min-w-32!">
<div> <div>
<PkiSyncStatusBadge status={syncStatus} /> <div className="flex w-full items-center">
</div> <p className="truncate">{name}</p>
</Tooltip> {description && (
)} <Tooltip content={description}>
{!isAutoSyncEnabled && ( <FontAwesomeIcon
<Tooltip icon={faInfoCircle}
className="text-xs" size="xs"
content="Auto-Sync is disabled. Certificate changes in the PKI subscriber will not be automatically synced to the destination." className="ml-1 text-mineshaft-400"
> />
<Badge variant="neutral">
<BanIcon />
{!syncStatus && "Auto-Sync Disabled"}
</Badge>
</Tooltip>
)}
{syncOption?.canImportCertificates && <PkiSyncImportStatusBadge mini pkiSync={pkiSync} />}
<PkiSyncRemoveStatusBadge mini pkiSync={pkiSync} />
</div>
</Td>
<Td>
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
onClick={(e) => {
e.stopPropagation();
handleCopyId();
}}
>
Copy Sync ID
</DropdownMenuItem>
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.SyncCertificates}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faRotate} />}
onClick={(e) => {
e.stopPropagation();
onTriggerSyncCertificates(pkiSync);
}}
isDisabled={!isAllowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Manually trigger a sync for this ${destinationName} destination.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span> Trigger Sync</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip> </Tooltip>
</DropdownMenuItem> )}
</div>
<p className="truncate text-xs leading-4 text-bunker-300">
{destinationDetails.name}
</p>
</div>
</Td>
<PkiSyncDestinationCol pkiSync={pkiSync} />
<Td>
<div className="flex items-center gap-1">
{syncStatus && (
<Tooltip
position="left"
className="max-w-sm"
content={
[PkiSyncStatus.Succeeded, PkiSyncStatus.Failed].includes(syncStatus) ? (
<div className="flex flex-col gap-2 py-1 whitespace-normal">
{lastSyncedAt && (
<div>
<div
className={`mb-2 flex self-start ${syncStatus === PkiSyncStatus.Failed ? "text-yellow" : "text-green"}`}
>
<FontAwesomeIcon
icon={faCalendarCheck}
className="ml-1 pt-0.5 pr-1.5 text-sm"
/>
<div className="text-xs">Last Synced</div>
</div>
<div className="rounded-sm bg-mineshaft-600 p-2 text-xs">
{format(new Date(lastSyncedAt), "yyyy-MM-dd, hh:mm aaa")}
</div>
</div>
)}
{failureMessage && (
<div>
<div className="mb-2 flex self-start text-red">
<FontAwesomeIcon
icon={faXmark}
className="ml-1 pt-0.5 pr-1.5 text-sm"
/>
<div className="text-xs">Failure Reason</div>
</div>
<div className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">
{failureMessage}
</div>
</div>
)}
</div>
) : undefined
}
>
<div>
<PkiSyncStatusBadge status={syncStatus} />
</div>
</Tooltip>
)} )}
</ProjectPermissionCan> {!isAutoSyncEnabled && (
{syncOption?.canImportCertificates && ( <Tooltip
<ProjectPermissionCan className="text-xs"
I={ProjectPermissionPkiSyncActions.ImportCertificates} content="Auto-Sync is disabled. Certificate changes in the PKI subscriber will not be automatically synced to the destination."
a={permissionSubject} >
> <Badge variant="neutral">
{(isAllowed: boolean) => ( <BanIcon />
{!syncStatus && "Auto-Sync Disabled"}
</Badge>
</Tooltip>
)}
{syncOption?.canImportCertificates && (
<PkiSyncImportStatusBadge mini pkiSync={pkiSync} />
)}
<PkiSyncRemoveStatusBadge mini pkiSync={pkiSync} />
</div>
</Td>
<Td>
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<DropdownMenuItem <DropdownMenuItem
icon={<FontAwesomeIcon icon={faDownload} />} icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onTriggerImportCertificates(pkiSync); handleCopyId();
}} }}
isDisabled={!isAllowed}
> >
<Tooltip Copy Sync ID
position="left"
sideOffset={42}
content={`Import certificates from this ${destinationName} destination into Infisical.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Import Certificates</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem> </DropdownMenuItem>
)} <ProjectPermissionCan
</ProjectPermissionCan> I={ProjectPermissionPkiSyncActions.SyncCertificates}
)} a={permissionSubject}
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.RemoveCertificates}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faEraser} />}
onClick={(e) => {
e.stopPropagation();
onTriggerRemoveCertificates(pkiSync);
}}
isDisabled={!isAllowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Remove certificates synced by Infisical from this ${destinationName} destination.`}
> >
<div className="flex h-full w-full items-center justify-between gap-1"> {(allowed: boolean) => (
<span>Remove Certificates</span> <DropdownMenuItem
<FontAwesomeIcon icon={<FontAwesomeIcon icon={faRotate} />}
className="text-bunker-300" onClick={(e) => {
size="sm" e.stopPropagation();
icon={faInfoCircle} onTriggerSyncCertificates(pkiSync);
/> }}
</div> isDisabled={!allowed}
</Tooltip> >
</DropdownMenuItem> <Tooltip
)} position="left"
</ProjectPermissionCan> sideOffset={42}
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}> content={`Manually trigger a sync for this ${destinationName} destination.`}
{(isAllowed: boolean) => ( >
<DropdownMenuItem <div className="flex h-full w-full items-center justify-between gap-1">
isDisabled={!isAllowed} <span> Trigger Sync</span>
icon={<FontAwesomeIcon icon={isAutoSyncEnabled ? faToggleOff : faToggleOn} />} <FontAwesomeIcon
onClick={(e) => { className="text-bunker-300"
e.stopPropagation(); size="sm"
onToggleEnable(pkiSync); icon={faInfoCircle}
}} />
> </div>
{isAutoSyncEnabled ? "Disable" : "Enable"} Auto-Sync </Tooltip>
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan {syncOption?.canImportCertificates && (
I={ProjectPermissionPkiSyncActions.Delete} <ProjectPermissionCan
a={permissionSubject} I={ProjectPermissionPkiSyncActions.ImportCertificates}
> a={permissionSubject}
{(isAllowed: boolean) => ( >
<DropdownMenuItem {(allowed: boolean) => (
isDisabled={!isAllowed} <DropdownMenuItem
icon={<FontAwesomeIcon icon={faTrash} />} icon={<FontAwesomeIcon icon={faDownload} />}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onDelete(pkiSync); onTriggerImportCertificates(pkiSync);
}} }}
> isDisabled={!allowed}
Delete Sync >
</DropdownMenuItem> <Tooltip
)} position="left"
</ProjectPermissionCan> sideOffset={42}
</DropdownMenuContent> content={`Import certificates from this ${destinationName} destination into Infisical.`}
</DropdownMenu> >
</Tooltip> <div className="flex h-full w-full items-center justify-between gap-1">
</Td> <span>Import Certificates</span>
</Tr> <FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.RemoveCertificates}
a={permissionSubject}
>
{(allowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faEraser} />}
onClick={(e) => {
e.stopPropagation();
onTriggerRemoveCertificates(pkiSync);
}}
isDisabled={!allowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Remove certificates synced by Infisical from this ${destinationName} destination.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Remove Certificates</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.Edit}
a={permissionSubject}
>
{(allowed: boolean) => (
<DropdownMenuItem
isDisabled={!allowed}
icon={
<FontAwesomeIcon icon={isAutoSyncEnabled ? faToggleOff : faToggleOn} />
}
onClick={(e) => {
e.stopPropagation();
onToggleEnable(pkiSync);
}}
>
{isAutoSyncEnabled ? "Disable" : "Enable"} Auto-Sync
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.Delete}
a={permissionSubject}
>
{(allowed: boolean) => (
<DropdownMenuItem
isDisabled={!allowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={(e) => {
e.stopPropagation();
onDelete(pkiSync);
}}
>
Delete Sync
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
);
}}
</ProjectPermissionCan>
); );
}; };
@@ -53,7 +53,7 @@ type Props = {
}; };
export const PkiSyncActionTriggers = ({ pkiSync }: Props) => { export const PkiSyncActionTriggers = ({ pkiSync }: Props) => {
const { destination, subscriberId, projectId, id } = pkiSync; const { destination, projectId, id } = pkiSync;
const navigate = useNavigate(); const navigate = useNavigate();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
@@ -112,7 +112,8 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => {
}, [updatePkiSyncMutation, id, projectId, pkiSync.isAutoSyncEnabled]); }, [updatePkiSyncMutation, id, projectId, pkiSync.isAutoSyncEnabled]);
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: subscriberId || "" subscriberName: destinationName,
name: pkiSync.name
}); });
return ( return (
@@ -32,6 +32,7 @@ import {
import { Badge } from "@app/components/v3"; import { Badge } from "@app/components/v3";
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { useListPkiSyncCertificates, useRemoveCertificatesFromPkiSync } from "@app/hooks/api"; import { useListPkiSyncCertificates, useRemoveCertificatesFromPkiSync } from "@app/hooks/api";
import { CertificateSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs"; import { CertificateSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
@@ -84,8 +85,11 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
const totalCount = data?.totalCount || 0; const totalCount = data?.totalCount || 0;
const removeCertificatesFromSync = useRemoveCertificatesFromPkiSync(); const removeCertificatesFromSync = useRemoveCertificatesFromPkiSync();
const destinationName = PKI_SYNC_MAP[pkiSync.destination].name;
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: pkiSync.subscriberId || "" subscriberName: destinationName,
name: pkiSync.name
}); });
const handleRemoveCertificate = async (certificateId: string) => { const handleRemoveCertificate = async (certificateId: string) => {
@@ -31,7 +31,7 @@ type Props = {
}; };
export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props) => { export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props) => {
const { destination, subscriberId } = pkiSync; const { destination } = pkiSync;
const destinationDetails = PKI_SYNC_MAP[destination]; const destinationDetails = PKI_SYNC_MAP[destination];
@@ -55,7 +55,8 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
} }
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: subscriberId || "" subscriberName: destinationDetails.name,
name: pkiSync.name
}); });
return ( return (
@@ -10,6 +10,7 @@ import { PkiSyncStatusBadge } from "@app/components/pki-syncs";
import { IconButton } from "@app/components/v2"; import { IconButton } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs"; import { PkiSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
const GenericFieldLabel = ({ const GenericFieldLabel = ({
@@ -33,8 +34,7 @@ type Props = {
}; };
export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => { export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => {
const { syncStatus, lastSyncMessage, lastSyncedAt, name, description, subscriberId, subscriber } = const { syncStatus, lastSyncMessage, lastSyncedAt, name, description, subscriber } = pkiSync;
pkiSync;
const failureMessage = useMemo(() => { const failureMessage = useMemo(() => {
if (syncStatus === PkiSyncStatus.Failed) { if (syncStatus === PkiSyncStatus.Failed) {
@@ -50,8 +50,11 @@ export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => {
return null; return null;
}, [syncStatus, lastSyncMessage]); }, [syncStatus, lastSyncMessage]);
const destinationName = PKI_SYNC_MAP[pkiSync.destination].name;
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: subscriber?.id || subscriberId || "" subscriberName: destinationName,
name: pkiSync.name
}); });
return ( return (
@@ -7,6 +7,7 @@ import { IconButton } from "@app/components/v2";
import { Badge } from "@app/components/v3"; import { Badge } from "@app/components/v3";
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs"; import { PkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs";
const GenericFieldLabel = ({ const GenericFieldLabel = ({
@@ -35,9 +36,11 @@ export const PkiSyncFieldMappingsSection = ({ pkiSync, onEditMappings }: Props)
} }
const fieldMappings = pkiSync.syncOptions?.fieldMappings; const fieldMappings = pkiSync.syncOptions?.fieldMappings;
const destinationName = PKI_SYNC_MAP[pkiSync.destination].name;
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: pkiSync.subscriberId || "" subscriberName: destinationName,
name: pkiSync.name
}); });
return ( return (
@@ -7,6 +7,7 @@ import { IconButton } from "@app/components/v2";
import { Badge } from "@app/components/v3"; import { Badge } from "@app/components/v3";
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { TPkiSync } from "@app/hooks/api/pkiSyncs"; import { TPkiSync } from "@app/hooks/api/pkiSyncs";
const GenericFieldLabel = ({ const GenericFieldLabel = ({
@@ -34,8 +35,11 @@ export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => {
syncOptions: { canRemoveCertificates } syncOptions: { canRemoveCertificates }
} = pkiSync; } = pkiSync;
const destinationName = PKI_SYNC_MAP[pkiSync.destination].name;
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: pkiSync.subscriberId || "" subscriberName: destinationName,
name: pkiSync.name
}); });
return ( return (
@@ -9,6 +9,7 @@ import { IconButton, Tooltip } from "@app/components/v2";
import { Badge } from "@app/components/v3"; import { Badge } from "@app/components/v3";
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { TPkiSync } from "@app/hooks/api/pkiSyncs"; import { TPkiSync } from "@app/hooks/api/pkiSyncs";
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => ( const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
@@ -26,8 +27,11 @@ type Props = {
export const PkiSyncSourceSection = ({ pkiSync, onEditSource }: Props) => { export const PkiSyncSourceSection = ({ pkiSync, onEditSource }: Props) => {
const { subscriberId, subscriber } = pkiSync; const { subscriberId, subscriber } = pkiSync;
const destinationName = PKI_SYNC_MAP[pkiSync.destination].name;
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: subscriberId || "" subscriberName: destinationName,
name: pkiSync.name
}); });
return ( return (
@@ -2,8 +2,15 @@ import { useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { PermissionDeniedBanner } from "@app/components/permissions";
import { ContentLoader, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; 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 { ProjectType } from "@app/hooks/api/projects/types";
import { CertificateProfilesTab } from "./components/CertificateProfilesTab"; import { CertificateProfilesTab } from "./components/CertificateProfilesTab";
@@ -20,8 +27,22 @@ enum TabSections {
export const PoliciesPage = () => { export const PoliciesPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentProject } = useProject(); const { currentProject } = useProject();
const { permission } = useProjectPermission();
const [activeTab, setActiveTab] = useState(TabSections.CertificateProfiles); 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) { if (!currentProject) {
return <ContentLoader />; return <ContentLoader />;
} }
@@ -56,15 +77,19 @@ export const PoliciesPage = () => {
</TabList> </TabList>
<TabPanel value={TabSections.CertificateProfiles}> <TabPanel value={TabSections.CertificateProfiles}>
<CertificateProfilesTab /> {canReadCertificateProfiles ? <CertificateProfilesTab /> : <PermissionDeniedBanner />}
</TabPanel> </TabPanel>
<TabPanel value={TabSections.CertificateTemplatesV2}> <TabPanel value={TabSections.CertificateTemplatesV2}>
<CertificateTemplatesV2Tab /> {canReadCertificateTemplates ? (
<CertificateTemplatesV2Tab />
) : (
<PermissionDeniedBanner />
)}
</TabPanel> </TabPanel>
<TabPanel value={TabSections.Certificates}> <TabPanel value={TabSections.Certificates}>
<CertificatesTab /> {canReadCertificates ? <CertificatesTab /> : <PermissionDeniedBanner />}
</TabPanel> </TabPanel>
</Tabs> </Tabs>
</div> </div>
@@ -4,10 +4,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2"; import { Button, DeleteActionModal } from "@app/components/v2";
import { useProjectPermission } from "@app/context";
import { import {
ProjectPermissionActions, ProjectPermissionCertificateProfileActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types"; } from "@app/context/ProjectPermissionContext/types";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
@@ -21,8 +21,6 @@ import { ProfileList } from "./ProfileList";
import { RevealAcmeEabSecretModal } from "./RevealAcmeEabSecretModal"; import { RevealAcmeEabSecretModal } from "./RevealAcmeEabSecretModal";
export const CertificateProfilesTab = () => { export const CertificateProfilesTab = () => {
const { permission } = useProjectPermission();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@@ -35,11 +33,6 @@ export const CertificateProfilesTab = () => {
const deleteProfile = useDeleteCertificateProfile(); const deleteProfile = useDeleteCertificateProfile();
const canCreateProfile = permission.can(
ProjectPermissionActions.Create,
ProjectPermissionSub.CertificateAuthorities
);
const handleCreateProfile = () => { const handleCreateProfile = () => {
setIsCreateModalOpen(true); setIsCreateModalOpen(true);
}; };
@@ -84,16 +77,22 @@ export const CertificateProfilesTab = () => {
</p> </p>
</div> </div>
{canCreateProfile && ( <ProjectPermissionCan
<Button I={ProjectPermissionCertificateProfileActions.Create}
colorSchema="primary" a={ProjectPermissionSub.CertificateProfiles}
type="button" >
leftIcon={<FontAwesomeIcon icon={faPlus} />} {(isAllowed) => (
onClick={handleCreateProfile} <Button
> isDisabled={!isAllowed}
Create Profile colorSchema="primary"
</Button> type="button"
)} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={handleCreateProfile}
>
Create Profile
</Button>
)}
</ProjectPermissionCan>
</div> </div>
<ProfileList <ProfileList
@@ -1,5 +1,6 @@
/* eslint-disable no-nested-ternary */ /* eslint-disable no-nested-ternary */
import { useCallback } from "react"; import { useCallback } from "react";
import { subject } from "@casl/ability";
import { import {
faCheck, faCheck,
faCircleInfo, faCircleInfo,
@@ -13,6 +14,7 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -23,9 +25,7 @@ import {
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge } from "@app/components/v3"; import { Badge } from "@app/components/v3";
import { useProjectPermission } from "@app/context";
import { import {
ProjectPermissionActions,
ProjectPermissionCertificateProfileActions, ProjectPermissionCertificateProfileActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types"; } from "@app/context/ProjectPermissionContext/types";
@@ -48,8 +48,6 @@ export const ProfileRow = ({
onRevealProfileAcmeEabSecret, onRevealProfileAcmeEabSecret,
onDeleteProfile onDeleteProfile
}: Props) => { }: Props) => {
const { permission } = useProjectPermission();
const { data: caData } = useGetInternalCaById(profile.caId ?? ""); const { data: caData } = useGetInternalCaById(profile.caId ?? "");
const { popUp, handlePopUpToggle } = usePopUp(["issueCertificate"] as const); const { popUp, handlePopUpToggle } = usePopUp(["issueCertificate"] as const);
@@ -72,26 +70,6 @@ export const ProfileRow = ({
templateId: profile.certificateTemplateId 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 getEnrollmentTypeBadge = (enrollmentType: string) => {
const config = { const config = {
api: { variant: "ghost" as const, label: "API" }, api: { variant: "ghost" as const, label: "API" },
@@ -152,50 +130,82 @@ export const ProfileRow = ({
> >
Copy Profile ID Copy Profile ID
</DropdownMenuItem> </DropdownMenuItem>
{canEditProfile && ( <ProjectPermissionCan
<DropdownMenuItem I={ProjectPermissionCertificateProfileActions.Edit}
onClick={(e) => { a={subject(ProjectPermissionSub.CertificateProfiles, { slug: profile.slug })}
e.stopPropagation(); >
onEditProfile(profile); {(isAllowed) =>
}} isAllowed && (
icon={<FontAwesomeIcon icon={faEdit} className="w-3" />} <DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onEditProfile(profile);
}}
icon={<FontAwesomeIcon icon={faEdit} className="w-3" />}
>
Edit Profile
</DropdownMenuItem>
)
}
</ProjectPermissionCan>
{profile.enrollmentType === "acme" && (
<ProjectPermissionCan
I={ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret}
a={subject(ProjectPermissionSub.CertificateProfiles, { slug: profile.slug })}
> >
Edit Profile {(isAllowed) =>
</DropdownMenuItem> isAllowed && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onRevealProfileAcmeEabSecret(profile);
}}
icon={<FontAwesomeIcon icon={faEye} className="w-3" />}
>
Reveal ACME EAB
</DropdownMenuItem>
)
}
</ProjectPermissionCan>
)} )}
{canRevealProfileAcmeEabSecret && profile.enrollmentType === "acme" && ( {profile.enrollmentType === "api" && (
<DropdownMenuItem <ProjectPermissionCan
onClick={(e) => { I={ProjectPermissionCertificateProfileActions.IssueCert}
e.stopPropagation(); a={subject(ProjectPermissionSub.CertificateProfiles, { slug: profile.slug })}
onRevealProfileAcmeEabSecret(profile);
}}
icon={<FontAwesomeIcon icon={faEye} className="w-3" />}
> >
Reveal ACME EAB {(isAllowed) =>
</DropdownMenuItem> isAllowed && (
)} <DropdownMenuItem
{canIssueCertificate && profile.enrollmentType === "api" && ( onClick={(e) => {
<DropdownMenuItem e.stopPropagation();
onClick={(e) => { handlePopUpToggle("issueCertificate");
e.stopPropagation(); }}
handlePopUpToggle("issueCertificate"); icon={<FontAwesomeIcon icon={faPlus} className="w-3" />}
}} >
icon={<FontAwesomeIcon icon={faPlus} className="w-3" />} Request Certificate
> </DropdownMenuItem>
Request Certificate )
</DropdownMenuItem> }
)} </ProjectPermissionCan>
{canDeleteProfile && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDeleteProfile(profile);
}}
icon={<FontAwesomeIcon icon={faTrash} className="w-3" />}
>
Delete Profile
</DropdownMenuItem>
)} )}
<ProjectPermissionCan
I={ProjectPermissionCertificateProfileActions.Delete}
a={subject(ProjectPermissionSub.CertificateProfiles, { slug: profile.slug })}
>
{(isAllowed) =>
isAllowed && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDeleteProfile(profile);
}}
icon={<FontAwesomeIcon icon={faTrash} className="w-3" />}
>
Delete Profile
</DropdownMenuItem>
)
}
</ProjectPermissionCan>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<CertificateIssuanceModal <CertificateIssuanceModal
@@ -3,8 +3,8 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2"; import { Button, DeleteActionModal } from "@app/components/v2";
import { useProjectPermission } from "@app/context";
import { import {
ProjectPermissionPkiTemplateActions, ProjectPermissionPkiTemplateActions,
ProjectPermissionSub ProjectPermissionSub
@@ -16,8 +16,6 @@ import { CreateTemplateModal } from "./CreateTemplateModal";
import { TemplateList } from "./TemplateList"; import { TemplateList } from "./TemplateList";
export const CertificateTemplatesV2Tab = () => { export const CertificateTemplatesV2Tab = () => {
const { permission } = useProjectPermission();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@@ -26,11 +24,6 @@ export const CertificateTemplatesV2Tab = () => {
const deleteTemplateV2 = useDeleteCertificateTemplateV2WithPolicies(); const deleteTemplateV2 = useDeleteCertificateTemplateV2WithPolicies();
const canCreateTemplate = permission.can(
ProjectPermissionPkiTemplateActions.Create,
ProjectPermissionSub.CertificateTemplates
);
const handleCreateTemplate = () => { const handleCreateTemplate = () => {
setIsCreateModalOpen(true); setIsCreateModalOpen(true);
}; };
@@ -70,16 +63,22 @@ export const CertificateTemplatesV2Tab = () => {
</p> </p>
</div> </div>
{canCreateTemplate && ( <ProjectPermissionCan
<Button I={ProjectPermissionPkiTemplateActions.Create}
colorSchema="primary" a={ProjectPermissionSub.CertificateTemplates}
type="button" >
leftIcon={<FontAwesomeIcon icon={faPlus} />} {(isAllowed) => (
onClick={handleCreateTemplate} <Button
> isDisabled={!isAllowed}
Create Template colorSchema="primary"
</Button> type="button"
)} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={handleCreateTemplate}
>
Create Template
</Button>
)}
</ProjectPermissionCan>
</div> </div>
<TemplateList onEditTemplate={handleEditTemplate} onDeleteTemplate={handleDeleteTemplate} /> <TemplateList onEditTemplate={handleEditTemplate} onDeleteTemplate={handleDeleteTemplate} />
@@ -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 } FormData & { preset: TemplatePresetId }
>({ >({
resolver: zodResolver(templateSchema), resolver: zodResolver(templateSchema),
@@ -286,10 +286,11 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
}; };
const watchedPreset = watch("preset") || TEMPLATE_PRESET_IDS.CUSTOM; const watchedPreset = watch("preset") || TEMPLATE_PRESET_IDS.CUSTOM;
const handlePresetChange = (presetId: TemplatePresetId) => { const handlePresetChange = async (presetId: TemplatePresetId) => {
setValue("preset", presetId); setValue("preset", presetId);
if (presetId === TEMPLATE_PRESET_IDS.CUSTOM) { if (presetId === TEMPLATE_PRESET_IDS.CUSTOM) {
await trigger();
return; return;
} }
@@ -313,6 +314,8 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
if (selectedPreset.formData.keyAlgorithm) { if (selectedPreset.formData.keyAlgorithm) {
setValue("keyAlgorithm", selectedPreset.formData.keyAlgorithm); setValue("keyAlgorithm", selectedPreset.formData.keyAlgorithm);
} }
await trigger();
} }
}; };
@@ -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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -17,11 +28,12 @@ import {
Tooltip, Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useProject, useProjectPermission } from "@app/context"; import { useProject } from "@app/context";
import { import {
ProjectPermissionPkiTemplateActions, ProjectPermissionPkiTemplateActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types"; } from "@app/context/ProjectPermissionContext/types";
import { useToggle } from "@app/hooks";
import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries"; import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries";
import { TCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/types"; import { TCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/types";
@@ -31,8 +43,8 @@ interface Props {
} }
export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => { export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
const { permission } = useProjectPermission();
const { currentProject } = useProject(); const { currentProject } = useProject();
const [isIdCopied, setIsIdCopied] = useToggle(false);
const { data, isLoading } = useListCertificateTemplatesV2({ const { data, isLoading } = useListCertificateTemplatesV2({
projectId: currentProject?.id || "", projectId: currentProject?.id || "",
@@ -42,20 +54,25 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
const templates = data?.certificateTemplates || []; 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) { if (!currentProject?.id) {
return null; return null;
} }
const canEditTemplate = permission.can(
ProjectPermissionPkiTemplateActions.Edit,
ProjectPermissionSub.CertificateTemplates
);
const canDeleteTemplate = permission.can(
ProjectPermissionPkiTemplateActions.Delete,
ProjectPermissionSub.CertificateTemplates
);
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString(); return new Date(dateString).toLocaleDateString();
}; };
@@ -110,28 +127,55 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
{canEditTemplate && ( <DropdownMenuItem
<DropdownMenuItem onClick={(e) => {
onClick={(e) => { e.stopPropagation();
e.stopPropagation(); handleCopyId(template.id);
onEditTemplate(template); }}
}} icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
icon={<FontAwesomeIcon icon={faEdit} />} >
> Copy Template ID
Edit Template </DropdownMenuItem>
</DropdownMenuItem> <ProjectPermissionCan
)} I={ProjectPermissionPkiTemplateActions.Edit}
{canDeleteTemplate && ( a={subject(ProjectPermissionSub.CertificateTemplates, {
<DropdownMenuItem name: template.name
onClick={(e) => { })}
e.stopPropagation(); >
onDeleteTemplate(template); {(isAllowed) =>
}} isAllowed && (
icon={<FontAwesomeIcon icon={faTrash} />} <DropdownMenuItem
> onClick={(e) => {
Delete Template e.stopPropagation();
</DropdownMenuItem> onEditTemplate(template);
)} }}
icon={<FontAwesomeIcon icon={faEdit} />}
>
Edit Template
</DropdownMenuItem>
)
}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Delete}
a={subject(ProjectPermissionSub.CertificateTemplates, {
name: template.name
})}
>
{(isAllowed) =>
isAllowed && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDeleteTemplate(template);
}}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete Template
</DropdownMenuItem>
)
}
</ProjectPermissionCan>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</Td> </Td>
@@ -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 (
<ConditionsFields
isDisabled={isDisabled}
subject={ProjectPermissionSub.CertificateAuthorities}
position={position}
selectOptions={[{ value: "name", label: "Name" }]}
/>
);
};
@@ -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 (
<ConditionsFields
isDisabled={isDisabled}
subject={ProjectPermissionSub.Certificates}
position={position}
selectOptions={[
{ value: "commonName", label: "Common Name" },
{ value: "altNames", label: "Subject Alternative Names" },
{ value: "serialNumber", label: "Serial Number" }
]}
/>
);
};
@@ -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 (
<ConditionsFields
isDisabled={isDisabled}
subject={ProjectPermissionSub.CertificateProfiles}
position={position}
selectOptions={[{ value: "slug", label: "Name" }]}
/>
);
};
@@ -36,6 +36,7 @@ export const renderOperatorSelectItems = (type: string) => {
return ( return (
<> <>
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem> <SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob Match</SelectItem> <SelectItem value={PermissionConditionOperators.$GLOB}>Glob Match</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem> <SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</> </>
@@ -13,7 +13,7 @@ export const PkiSyncPermissionConditions = ({ position = 0, isDisabled }: Props)
isDisabled={isDisabled} isDisabled={isDisabled}
subject={ProjectPermissionSub.PkiSyncs} subject={ProjectPermissionSub.PkiSyncs}
position={position} position={position}
selectOptions={[{ value: "subscriberName", label: "Subscriber Name" }]} selectOptions={[{ value: "name", label: "Sync Name" }]}
/> />
); );
}; };
@@ -7,6 +7,7 @@ import { Tooltip } from "@app/components/v2";
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCertificateAuthorityActions,
ProjectPermissionCertificateProfileActions, ProjectPermissionCertificateProfileActions,
ProjectPermissionCmekActions, ProjectPermissionCmekActions,
ProjectPermissionSub ProjectPermissionSub
@@ -56,7 +57,17 @@ const CertificatePolicyActionSchema = z.object({
[ProjectPermissionCertificateActions.Delete]: z.boolean().optional(), [ProjectPermissionCertificateActions.Delete]: z.boolean().optional(),
[ProjectPermissionCertificateActions.Edit]: z.boolean().optional(), [ProjectPermissionCertificateActions.Edit]: z.boolean().optional(),
[ProjectPermissionCertificateActions.Read]: 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({ const SecretPolicyActionSchema = z.object({
@@ -208,7 +219,6 @@ const PkiSubscriberPolicyActionSchema = z.object({
[ProjectPermissionPkiSubscriberActions.Create]: z.boolean().optional(), [ProjectPermissionPkiSubscriberActions.Create]: z.boolean().optional(),
[ProjectPermissionPkiSubscriberActions.Edit]: z.boolean().optional(), [ProjectPermissionPkiSubscriberActions.Edit]: z.boolean().optional(),
[ProjectPermissionPkiSubscriberActions.Delete]: z.boolean().optional(), [ProjectPermissionPkiSubscriberActions.Delete]: z.boolean().optional(),
[ProjectPermissionPkiSubscriberActions.IssueCert]: z.boolean().optional(),
[ProjectPermissionPkiSubscriberActions.ListCerts]: z.boolean().optional() [ProjectPermissionPkiSubscriberActions.ListCerts]: z.boolean().optional()
}); });
@@ -217,7 +227,6 @@ const PkiTemplatePolicyActionSchema = z.object({
[ProjectPermissionPkiTemplateActions.Create]: z.boolean().optional(), [ProjectPermissionPkiTemplateActions.Create]: z.boolean().optional(),
[ProjectPermissionPkiTemplateActions.Edit]: z.boolean().optional(), [ProjectPermissionPkiTemplateActions.Edit]: z.boolean().optional(),
[ProjectPermissionPkiTemplateActions.Delete]: z.boolean().optional(), [ProjectPermissionPkiTemplateActions.Delete]: z.boolean().optional(),
[ProjectPermissionPkiTemplateActions.IssueCert]: z.boolean().optional(),
[ProjectPermissionPkiTemplateActions.ListCerts]: z.boolean().optional() [ProjectPermissionPkiTemplateActions.ListCerts]: z.boolean().optional()
}); });
const CertificateProfilePolicyActionSchema = z.object({ const CertificateProfilePolicyActionSchema = z.object({
@@ -225,7 +234,9 @@ const CertificateProfilePolicyActionSchema = z.object({
[ProjectPermissionCertificateProfileActions.Create]: z.boolean().optional(), [ProjectPermissionCertificateProfileActions.Create]: z.boolean().optional(),
[ProjectPermissionCertificateProfileActions.Edit]: z.boolean().optional(), [ProjectPermissionCertificateProfileActions.Edit]: z.boolean().optional(),
[ProjectPermissionCertificateProfileActions.Delete]: 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({ const SecretEventsPolicyActionSchema = z.object({
@@ -381,8 +392,18 @@ export const projectRoleFormSchema = z.object({
[ProjectPermissionSub.Environments]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Environments]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.AuditLogs]: AuditLogsPolicyActionSchema.array().default([]), [ProjectPermissionSub.AuditLogs]: AuditLogsPolicyActionSchema.array().default([]),
[ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.CertificateAuthorities]: CertificateAuthorityPolicyActionSchema.extend({
[ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.array().default([]), inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.PkiSubscribers]: PkiSubscriberPolicyActionSchema.extend({ [ProjectPermissionSub.PkiSubscribers]: PkiSubscriberPolicyActionSchema.extend({
inverted: z.boolean().optional(), inverted: z.boolean().optional(),
conditions: ConditionSchema conditions: ConditionSchema
@@ -397,8 +418,12 @@ export const projectRoleFormSchema = z.object({
}) })
.array() .array()
.default([]), .default([]),
[ProjectPermissionSub.CertificateProfiles]: [ProjectPermissionSub.CertificateProfiles]: CertificateProfilePolicyActionSchema.extend({
CertificateProfilePolicyActionSchema.array().default([]), inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default( [ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default(
[] []
), ),
@@ -458,6 +483,9 @@ type TConditionalFields =
| ProjectPermissionSub.DynamicSecrets | ProjectPermissionSub.DynamicSecrets
| ProjectPermissionSub.PkiSubscribers | ProjectPermissionSub.PkiSubscribers
| ProjectPermissionSub.CertificateTemplates | ProjectPermissionSub.CertificateTemplates
| ProjectPermissionSub.CertificateAuthorities
| ProjectPermissionSub.Certificates
| ProjectPermissionSub.CertificateProfiles
| ProjectPermissionSub.SshHosts | ProjectPermissionSub.SshHosts
| ProjectPermissionSub.SecretRotation | ProjectPermissionSub.SecretRotation
| ProjectPermissionSub.Identity | ProjectPermissionSub.Identity
@@ -479,6 +507,9 @@ export const isConditionalSubjects = (
subject === ProjectPermissionSub.SecretRotation || subject === ProjectPermissionSub.SecretRotation ||
subject === ProjectPermissionSub.PkiSubscribers || subject === ProjectPermissionSub.PkiSubscribers ||
subject === ProjectPermissionSub.CertificateTemplates || subject === ProjectPermissionSub.CertificateTemplates ||
subject === ProjectPermissionSub.CertificateAuthorities ||
subject === ProjectPermissionSub.Certificates ||
subject === ProjectPermissionSub.CertificateProfiles ||
subject === ProjectPermissionSub.SecretSyncs || subject === ProjectPermissionSub.SecretSyncs ||
subject === ProjectPermissionSub.PkiSyncs || subject === ProjectPermissionSub.PkiSyncs ||
subject === ProjectPermissionSub.SecretEvents || subject === ProjectPermissionSub.SecretEvents ||
@@ -822,6 +853,31 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
return; 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 // for other subjects
const canRead = action.includes(ProjectPermissionActions.Read); const canRead = action.includes(ProjectPermissionActions.Read);
const canEdit = action.includes(ProjectPermissionActions.Edit); const canEdit = action.includes(ProjectPermissionActions.Edit);
@@ -873,17 +929,22 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
const canEdit = action.includes(ProjectPermissionCertificateActions.Edit); const canEdit = action.includes(ProjectPermissionCertificateActions.Edit);
const canDelete = action.includes(ProjectPermissionCertificateActions.Delete); const canDelete = action.includes(ProjectPermissionCertificateActions.Delete);
const canCreate = action.includes(ProjectPermissionCertificateActions.Create); const canCreate = action.includes(ProjectPermissionCertificateActions.Create);
const canImport = action.includes(ProjectPermissionCertificateActions.Import);
const canReadPrivateKey = action.includes(ProjectPermissionCertificateActions.ReadPrivateKey); 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 // from above statement we are sure it won't be undefined
if (canRead) formVal[subject]![0].read = true; formVal[subject]!.push({
if (canEdit) formVal[subject]![0].edit = true; [ProjectPermissionCertificateActions.Read]: canRead,
if (canCreate) formVal[subject]![0].create = true; [ProjectPermissionCertificateActions.Edit]: canEdit,
if (canDelete) formVal[subject]![0].delete = true; [ProjectPermissionCertificateActions.Create]: canCreate,
if (canReadPrivateKey) [ProjectPermissionCertificateActions.Delete]: canDelete,
formVal[subject]![0][ProjectPermissionCertificateActions.ReadPrivateKey] = true; [ProjectPermissionCertificateActions.ReadPrivateKey]: canReadPrivateKey,
[ProjectPermissionCertificateActions.Import]: canImport,
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted
});
return; return;
} }
@@ -1129,9 +1190,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
[ProjectPermissionPkiSubscriberActions.Read]: action.includes( [ProjectPermissionPkiSubscriberActions.Read]: action.includes(
ProjectPermissionPkiSubscriberActions.Read ProjectPermissionPkiSubscriberActions.Read
), ),
[ProjectPermissionPkiSubscriberActions.IssueCert]: action.includes(
ProjectPermissionPkiSubscriberActions.IssueCert
),
[ProjectPermissionPkiSubscriberActions.ListCerts]: action.includes( [ProjectPermissionPkiSubscriberActions.ListCerts]: action.includes(
ProjectPermissionPkiSubscriberActions.ListCerts ProjectPermissionPkiSubscriberActions.ListCerts
), ),
@@ -1157,9 +1215,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
[ProjectPermissionPkiTemplateActions.Read]: action.includes( [ProjectPermissionPkiTemplateActions.Read]: action.includes(
ProjectPermissionPkiTemplateActions.Read ProjectPermissionPkiTemplateActions.Read
), ),
[ProjectPermissionPkiTemplateActions.IssueCert]: action.includes(
ProjectPermissionPkiTemplateActions.IssueCert
),
[ProjectPermissionPkiTemplateActions.ListCerts]: action.includes( [ProjectPermissionPkiTemplateActions.ListCerts]: action.includes(
ProjectPermissionPkiTemplateActions.ListCerts ProjectPermissionPkiTemplateActions.ListCerts
), ),
@@ -1187,7 +1242,15 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
), ),
[ProjectPermissionCertificateProfileActions.IssueCert]: action.includes( [ProjectPermissionCertificateProfileActions.IssueCert]: action.includes(
ProjectPermissionCertificateProfileActions.IssueCert ProjectPermissionCertificateProfileActions.IssueCert
) ),
[ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret]: action.includes(
ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret
),
[ProjectPermissionCertificateProfileActions.RotateAcmeEabSecret]: action.includes(
ProjectPermissionCertificateProfileActions.RotateAcmeEabSecret
),
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted
}); });
return; return;
@@ -1196,7 +1259,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
if (subject === ProjectPermissionSub.PamAccounts) { if (subject === ProjectPermissionSub.PamAccounts) {
if (!formVal[subject]) formVal[subject] = []; if (!formVal[subject]) formVal[subject] = [];
formVal[subject].push({ formVal[subject]!.push({
[ProjectPermissionPamAccountActions.Access]: action.includes( [ProjectPermissionPamAccountActions.Access]: action.includes(
ProjectPermissionPamAccountActions.Access ProjectPermissionPamAccountActions.Access
), ),
@@ -1552,10 +1615,15 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.CertificateAuthorities]: { [ProjectPermissionSub.CertificateAuthorities]: {
title: "Certificate Authorities", title: "Certificate Authorities",
actions: [ actions: [
{ label: "Read", value: "read" }, { label: "Read", value: ProjectPermissionCertificateAuthorityActions.Read },
{ label: "Create", value: "create" }, { label: "Create", value: ProjectPermissionCertificateAuthorityActions.Create },
{ label: "Modify", value: "edit" }, { label: "Modify", value: ProjectPermissionCertificateAuthorityActions.Edit },
{ label: "Remove", value: "delete" } { label: "Remove", value: ProjectPermissionCertificateAuthorityActions.Delete },
{ label: "Renew", value: ProjectPermissionCertificateAuthorityActions.Renew },
{
label: "Sign Intermediate",
value: ProjectPermissionCertificateAuthorityActions.SignIntermediate
}
] ]
}, },
[ProjectPermissionSub.Certificates]: { [ProjectPermissionSub.Certificates]: {
@@ -1563,7 +1631,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
actions: [ actions: [
{ label: "Read", value: ProjectPermissionCertificateActions.Read }, { label: "Read", value: ProjectPermissionCertificateActions.Read },
{ label: "Read Private Key", value: ProjectPermissionCertificateActions.ReadPrivateKey }, { 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: "Modify", value: ProjectPermissionCertificateActions.Edit },
{ label: "Remove", value: ProjectPermissionCertificateActions.Delete } { label: "Remove", value: ProjectPermissionCertificateActions.Delete }
] ]
@@ -1575,7 +1644,6 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
{ label: "Create", value: ProjectPermissionPkiTemplateActions.Create }, { label: "Create", value: ProjectPermissionPkiTemplateActions.Create },
{ label: "Modify", value: ProjectPermissionPkiTemplateActions.Edit }, { label: "Modify", value: ProjectPermissionPkiTemplateActions.Edit },
{ label: "Remove", value: ProjectPermissionPkiTemplateActions.Delete }, { label: "Remove", value: ProjectPermissionPkiTemplateActions.Delete },
{ label: "Issue Certificates", value: ProjectPermissionPkiTemplateActions.IssueCert },
{ label: "List Certificates", value: ProjectPermissionPkiTemplateActions.ListCerts } { label: "List Certificates", value: ProjectPermissionPkiTemplateActions.ListCerts }
] ]
}, },
@@ -1586,7 +1654,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
{ label: "Create", value: ProjectPermissionCertificateProfileActions.Create }, { label: "Create", value: ProjectPermissionCertificateProfileActions.Create },
{ label: "Modify", value: ProjectPermissionCertificateProfileActions.Edit }, { label: "Modify", value: ProjectPermissionCertificateProfileActions.Edit },
{ label: "Remove", value: ProjectPermissionCertificateProfileActions.Delete }, { label: "Remove", value: ProjectPermissionCertificateProfileActions.Delete },
{ label: "Issue Certificates", value: ProjectPermissionCertificateProfileActions.IssueCert } { label: "Request Certificates", value: ProjectPermissionCertificateProfileActions.IssueCert }
] ]
}, },
[ProjectPermissionSub.SshCertificateAuthorities]: { [ProjectPermissionSub.SshCertificateAuthorities]: {
@@ -1642,7 +1710,6 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
{ label: "Create", value: ProjectPermissionPkiSubscriberActions.Create }, { label: "Create", value: ProjectPermissionPkiSubscriberActions.Create },
{ label: "Modify", value: ProjectPermissionPkiSubscriberActions.Edit }, { label: "Modify", value: ProjectPermissionPkiSubscriberActions.Edit },
{ label: "Remove", value: ProjectPermissionPkiSubscriberActions.Delete }, { label: "Remove", value: ProjectPermissionPkiSubscriberActions.Delete },
{ label: "Issue Certificate", value: ProjectPermissionPkiSubscriberActions.IssueCert },
{ label: "List Certificates", value: ProjectPermissionPkiSubscriberActions.ListCerts } { label: "List Certificates", value: ProjectPermissionPkiSubscriberActions.ListCerts }
] ]
}, },
@@ -21,6 +21,9 @@ import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { AddPoliciesButton } from "./AddPoliciesButton"; import { AddPoliciesButton } from "./AddPoliciesButton";
import { AppConnectionPermissionConditions } from "./AppConnectionPermissionConditions"; import { AppConnectionPermissionConditions } from "./AppConnectionPermissionConditions";
import { CertificateAuthorityPermissionConditions } from "./CertificateAuthorityPermissionConditions";
import { CertificatePermissionConditions } from "./CertificatePermissionConditions";
import { CertificateProfilePermissionConditions } from "./CertificateProfilePermissionConditions";
import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions"; import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions";
import { GeneralPermissionConditions } from "./GeneralPermissionConditions"; import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies"; import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
@@ -96,6 +99,18 @@ export const renderConditionalComponents = (
return <PamAccountPermissionConditions isDisabled={isDisabled} />; return <PamAccountPermissionConditions isDisabled={isDisabled} />;
} }
if (subject === ProjectPermissionSub.CertificateAuthorities) {
return <CertificateAuthorityPermissionConditions isDisabled={isDisabled} />;
}
if (subject === ProjectPermissionSub.Certificates) {
return <CertificatePermissionConditions isDisabled={isDisabled} />;
}
if (subject === ProjectPermissionSub.CertificateProfiles) {
return <CertificateProfilePermissionConditions isDisabled={isDisabled} />;
}
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />; return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
} }