mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 22:28:15 +00:00
Merge pull request #4980 from Infisical/feat/PKI-67
feature: refine PKI access control permissions
This commit is contained in:
+5
-2
@@ -4,7 +4,10 @@ import * as x509 from "@peculiar/x509";
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import {
|
||||
ProjectPermissionCertificateAuthorityActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { expandInternalCa } from "@app/services/certificate-authority/certificate-authority-fns";
|
||||
@@ -83,7 +86,7 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionCertificateAuthorityActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ProjectPermissionAppConnectionActions,
|
||||
ProjectPermissionAuditLogsActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCertificateAuthorityActions,
|
||||
ProjectPermissionCertificateProfileActions,
|
||||
ProjectPermissionCmekActions,
|
||||
ProjectPermissionCommitsActions,
|
||||
@@ -45,7 +46,6 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.Environments,
|
||||
ProjectPermissionSub.Tags,
|
||||
ProjectPermissionSub.IpAllowList,
|
||||
ProjectPermissionSub.CertificateAuthorities,
|
||||
ProjectPermissionSub.PkiAlerts,
|
||||
ProjectPermissionSub.PkiCollections,
|
||||
ProjectPermissionSub.SshCertificateAuthorities,
|
||||
@@ -68,6 +68,18 @@ const buildAdminPermissionRules = () => {
|
||||
|
||||
can([ProjectPermissionAuditLogsActions.Read], ProjectPermissionSub.AuditLogs);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionCertificateAuthorityActions.Read,
|
||||
ProjectPermissionCertificateAuthorityActions.Create,
|
||||
ProjectPermissionCertificateAuthorityActions.Edit,
|
||||
ProjectPermissionCertificateAuthorityActions.Delete,
|
||||
ProjectPermissionCertificateAuthorityActions.Renew,
|
||||
ProjectPermissionCertificateAuthorityActions.SignIntermediate
|
||||
],
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionPkiTemplateActions.Read,
|
||||
@@ -96,7 +108,8 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionCertificateActions.Edit,
|
||||
ProjectPermissionCertificateActions.Create,
|
||||
ProjectPermissionCertificateActions.Delete,
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||
ProjectPermissionCertificateActions.Import
|
||||
],
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
@@ -461,7 +474,7 @@ const buildMemberPermissionRules = () => {
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList);
|
||||
|
||||
// double check if all CRUD are needed for CA and Certificates
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities);
|
||||
can([ProjectPermissionCertificateAuthorityActions.Read], ProjectPermissionSub.CertificateAuthorities);
|
||||
can([ProjectPermissionPkiTemplateActions.Read], ProjectPermissionSub.CertificateTemplates);
|
||||
|
||||
can(
|
||||
@@ -469,7 +482,8 @@ const buildMemberPermissionRules = () => {
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionCertificateActions.Edit,
|
||||
ProjectPermissionCertificateActions.Create,
|
||||
ProjectPermissionCertificateActions.Delete
|
||||
ProjectPermissionCertificateActions.Delete,
|
||||
ProjectPermissionCertificateActions.Import
|
||||
],
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
@@ -600,7 +614,7 @@ const buildViewerPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||
can(ProjectPermissionAuditLogsActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||
can(ProjectPermissionCertificateAuthorityActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||
can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionPkiTemplateActions.Read, ProjectPermissionSub.CertificateTemplates);
|
||||
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||
|
||||
@@ -23,12 +23,22 @@ export enum ProjectPermissionCommitsActions {
|
||||
PerformRollback = "perform-rollback"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionCertificateAuthorityActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
Renew = "renew",
|
||||
SignIntermediate = "sign-intermediate"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionCertificateActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
ReadPrivateKey = "read-private-key"
|
||||
ReadPrivateKey = "read-private-key",
|
||||
Import = "import"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretActions {
|
||||
@@ -292,7 +302,8 @@ export type SecretSyncSubjectFields = {
|
||||
};
|
||||
|
||||
export type PkiSyncSubjectFields = {
|
||||
subscriberName: string;
|
||||
subscriberName?: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type DynamicSecretSubjectFields = {
|
||||
@@ -332,6 +343,26 @@ export type PkiSubscriberSubjectFields = {
|
||||
// (dangtony98): consider adding [commonName] as a subject field in the future
|
||||
};
|
||||
|
||||
export type CertificateAuthoritySubjectFields = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type CertificateSubjectFields = {
|
||||
commonName?: string;
|
||||
altNames?: string;
|
||||
serialNumber?: string;
|
||||
friendlyName?: string;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export type CertificateProfileSubjectFields = {
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type CertificateTemplateV2SubjectFields = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type AppConnectionSubjectFields = {
|
||||
connectionId: string;
|
||||
};
|
||||
@@ -399,8 +430,17 @@ export type ProjectPermissionSet =
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates]
|
||||
| [
|
||||
ProjectPermissionCertificateAuthorityActions,
|
||||
(
|
||||
| ProjectPermissionSub.CertificateAuthorities
|
||||
| (ForcedSubject<ProjectPermissionSub.CertificateAuthorities> & CertificateAuthoritySubjectFields)
|
||||
)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionSub.Certificates | (ForcedSubject<ProjectPermissionSub.Certificates> & CertificateSubjectFields)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
(
|
||||
@@ -454,7 +494,13 @@ export type ProjectPermissionSet =
|
||||
ProjectPermissionSub.PamAccounts | (ForcedSubject<ProjectPermissionSub.PamAccounts> & PamAccountSubjectFields)
|
||||
]
|
||||
| [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_PERMISSION_OPERATOR_SCHEMA = z.union([
|
||||
@@ -572,6 +618,17 @@ const SecretSyncConditionV2Schema = z
|
||||
|
||||
const PkiSyncConditionSchema = z
|
||||
.object({
|
||||
name: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
]),
|
||||
subscriberName: z.union([
|
||||
z.string(),
|
||||
z
|
||||
@@ -698,6 +755,7 @@ const PkiTemplateConditionSchema = z
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||
})
|
||||
@@ -749,6 +807,98 @@ const PamAccountConditionSchema = z
|
||||
})
|
||||
.partial();
|
||||
|
||||
const CertificateAuthorityConditionSchema = z
|
||||
.object({
|
||||
name: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
])
|
||||
})
|
||||
.partial();
|
||||
|
||||
const CertificateConditionSchema = z
|
||||
.object({
|
||||
commonName: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
]),
|
||||
altNames: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
]),
|
||||
serialNumber: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
]),
|
||||
friendlyName: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
]),
|
||||
status: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
])
|
||||
})
|
||||
.partial();
|
||||
|
||||
const CertificateProfileConditionSchema = z
|
||||
.object({
|
||||
slug: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
])
|
||||
})
|
||||
.partial();
|
||||
|
||||
const GeneralPermissionSchema = [
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
|
||||
@@ -828,18 +978,6 @@ const GeneralPermissionSchema = [
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.CertificateAuthorities).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Certificates).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z
|
||||
.literal(ProjectPermissionSub.SshCertificateAuthorities)
|
||||
@@ -1130,7 +1268,30 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateProfileActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
),
|
||||
conditions: CertificateProfileConditionSchema.describe(
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.CertificateAuthorities).describe("The entity this permission pertains to."),
|
||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateAuthorityActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
),
|
||||
conditions: CertificateAuthorityConditionSchema.describe(
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Certificates).describe("The entity this permission pertains to."),
|
||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
),
|
||||
conditions: CertificateConditionSchema.describe(
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
...GeneralPermissionSchema
|
||||
]);
|
||||
|
||||
@@ -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,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
actorOrgId: req.permission.orgId,
|
||||
isInternal: true
|
||||
});
|
||||
const caType = (ca?.externalCa?.type as CaType) ?? CaType.INTERNAL;
|
||||
useOrderFlow = caType !== CaType.INTERNAL;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { TableName } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, CryptographyError, NotFoundError } from "@app/lib/errors";
|
||||
import { ProcessedPermissionRules } from "@app/lib/knex/permission-filter-utils";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
@@ -715,11 +716,21 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
return castDbEntryToAcmeCertificateAuthority(updatedCa);
|
||||
};
|
||||
|
||||
const listCertificateAuthorities = async ({ projectId }: { projectId: string }) => {
|
||||
const cas = await certificateAuthorityDAL.findWithAssociatedCa({
|
||||
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId,
|
||||
[`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.ACME
|
||||
});
|
||||
const listCertificateAuthorities = async ({
|
||||
projectId,
|
||||
permissionFilters
|
||||
}: {
|
||||
projectId: string;
|
||||
permissionFilters?: ProcessedPermissionRules;
|
||||
}) => {
|
||||
const cas = await certificateAuthorityDAL.findWithAssociatedCa(
|
||||
{
|
||||
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId,
|
||||
[`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.ACME
|
||||
},
|
||||
{},
|
||||
permissionFilters
|
||||
);
|
||||
|
||||
return cas.map(castDbEntryToAcmeCertificateAuthority);
|
||||
};
|
||||
|
||||
+16
-5
@@ -5,6 +5,7 @@ import RE2 from "re2";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ProcessedPermissionRules } from "@app/lib/knex/permission-filter-utils";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
@@ -798,11 +799,21 @@ export const AzureAdCsCertificateAuthorityFns = ({
|
||||
return castDbEntryToAzureAdCsCertificateAuthority(updatedCa);
|
||||
};
|
||||
|
||||
const listCertificateAuthorities = async ({ projectId }: { projectId: string }) => {
|
||||
const cas = await certificateAuthorityDAL.findWithAssociatedCa({
|
||||
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId,
|
||||
[`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.AZURE_AD_CS
|
||||
});
|
||||
const listCertificateAuthorities = async ({
|
||||
projectId,
|
||||
permissionFilters
|
||||
}: {
|
||||
projectId: string;
|
||||
permissionFilters?: ProcessedPermissionRules;
|
||||
}) => {
|
||||
const cas = await certificateAuthorityDAL.findWithAssociatedCa(
|
||||
{
|
||||
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId,
|
||||
[`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.AZURE_AD_CS
|
||||
},
|
||||
{},
|
||||
permissionFilters
|
||||
);
|
||||
|
||||
return cas.map(castDbEntryToAzureAdCsCertificateAuthority);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,10 @@ import { TDbClient } from "@app/db";
|
||||
import { CertificateAuthoritiesSchema, TableName, TCertificateAuthorities } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex";
|
||||
import {
|
||||
applyProcessedPermissionRulesToQuery,
|
||||
type ProcessedPermissionRules
|
||||
} from "@app/lib/knex/permission-filter-utils";
|
||||
|
||||
export type TCertificateAuthorityDALFactory = ReturnType<typeof certificateAuthorityDALFactory>;
|
||||
|
||||
@@ -220,10 +224,11 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => {
|
||||
const findWithAssociatedCa = async (
|
||||
filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string; serialNumber?: string },
|
||||
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TCertificateAuthorities> = {},
|
||||
permissionFilters?: ProcessedPermissionRules,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const query = (tx || db.replicaNode())(TableName.CertificateAuthority)
|
||||
let query = (tx || db.replicaNode())(TableName.CertificateAuthority)
|
||||
.leftJoin(
|
||||
TableName.InternalCertificateAuthority,
|
||||
`${TableName.CertificateAuthority}.id`,
|
||||
@@ -268,6 +273,14 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => {
|
||||
db.ref("appConnectionId").withSchema(TableName.ExternalCertificateAuthority).as("externalAppConnectionId")
|
||||
);
|
||||
|
||||
if (permissionFilters) {
|
||||
query = applyProcessedPermissionRulesToQuery(
|
||||
query,
|
||||
TableName.CertificateAuthority,
|
||||
permissionFilters
|
||||
) as typeof query;
|
||||
}
|
||||
|
||||
if (limit) void query.limit(limit);
|
||||
if (offset) void query.offset(offset);
|
||||
if (sort) {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, TableName } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import {
|
||||
ProjectPermissionCertificateAuthorityActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
@@ -142,8 +146,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.Create,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, { name })
|
||||
);
|
||||
|
||||
if (type === CaType.INTERNAL) {
|
||||
@@ -212,8 +216,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.Read,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name })
|
||||
);
|
||||
|
||||
if (type === CaType.INTERNAL) {
|
||||
@@ -227,6 +231,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
id: certificateAuthority.id,
|
||||
type,
|
||||
enableDirectIssuance: certificateAuthority.enableDirectIssuance,
|
||||
subject: ProjectPermissionSub.CertificateAuthorities,
|
||||
name: certificateAuthority.name,
|
||||
projectId: certificateAuthority.projectId,
|
||||
configuration: certificateAuthority.internalCa,
|
||||
@@ -275,8 +280,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.Read,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, { name: caName })
|
||||
);
|
||||
|
||||
if (type === CaType.INTERNAL) {
|
||||
@@ -328,15 +333,25 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionCertificateAuthorityActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const permissionFilters = getProcessedPermissionRules(
|
||||
permission,
|
||||
ProjectPermissionCertificateAuthorityActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
if (type === CaType.INTERNAL) {
|
||||
const cas = await certificateAuthorityDAL.findWithAssociatedCa({
|
||||
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId,
|
||||
$notNull: [`${TableName.InternalCertificateAuthority}.id` as "id"]
|
||||
});
|
||||
const cas = await certificateAuthorityDAL.findWithAssociatedCa(
|
||||
{
|
||||
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId,
|
||||
$notNull: [`${TableName.InternalCertificateAuthority}.id` as "id"]
|
||||
},
|
||||
{},
|
||||
permissionFilters
|
||||
);
|
||||
|
||||
return cas
|
||||
.filter((ca): ca is typeof ca & { internalCa: NonNullable<typeof ca.internalCa> } => Boolean(ca.internalCa))
|
||||
@@ -352,11 +367,11 @@ export const certificateAuthorityServiceFactory = ({
|
||||
}
|
||||
|
||||
if (type === CaType.ACME) {
|
||||
return acmeFns.listCertificateAuthorities({ projectId });
|
||||
return acmeFns.listCertificateAuthorities({ projectId, permissionFilters });
|
||||
}
|
||||
|
||||
if (type === CaType.AZURE_AD_CS) {
|
||||
return azureAdCsFns.listCertificateAuthorities({ projectId });
|
||||
return azureAdCsFns.listCertificateAuthorities({ projectId, permissionFilters });
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
@@ -383,8 +398,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.Edit,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name })
|
||||
);
|
||||
|
||||
if (type === CaType.INTERNAL) {
|
||||
@@ -459,8 +474,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.Delete,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name })
|
||||
);
|
||||
|
||||
if (!certificateAuthority.internalCa?.id && type === CaType.INTERNAL) {
|
||||
@@ -524,8 +539,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.Edit,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name })
|
||||
);
|
||||
|
||||
if (type === CaType.INTERNAL) {
|
||||
@@ -606,8 +621,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.Delete,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, { name: certificateAuthority.name })
|
||||
);
|
||||
|
||||
if (!certificateAuthority.internalCa?.id && type === CaType.INTERNAL) {
|
||||
@@ -662,6 +677,13 @@ export const certificateAuthorityServiceFactory = ({
|
||||
actorAuthMethod: OrgServiceActor["authMethod"];
|
||||
actorOrgId?: string;
|
||||
}) => {
|
||||
const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
||||
|
||||
if (!certificateAuthority)
|
||||
throw new NotFoundError({
|
||||
message: `Could not find certificate authority with id "${caId}"`
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
@@ -672,8 +694,10 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.Read,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, {
|
||||
name: certificateAuthority.name
|
||||
})
|
||||
);
|
||||
|
||||
return azureAdCsFns.getTemplates({
|
||||
@@ -687,32 +711,38 @@ export const certificateAuthorityServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
isInternal
|
||||
}: {
|
||||
caId: string;
|
||||
actor: OrgServiceActor["type"];
|
||||
actorId: string;
|
||||
actorAuthMethod: OrgServiceActor["authMethod"];
|
||||
actorOrgId?: string;
|
||||
isInternal?: boolean;
|
||||
}) => {
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
||||
if (!ca) {
|
||||
throw new NotFoundError({ message: "CA not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
if (!isInternal) {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateAuthorityActions.Read,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, {
|
||||
name: ca.name
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return ca;
|
||||
};
|
||||
|
||||
+21
-21
@@ -7,8 +7,8 @@ import { Knex } from "knex";
|
||||
import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCertificateAuthorityActions,
|
||||
ProjectPermissionCertificateProfileActions,
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
ProjectPermissionSub
|
||||
@@ -174,8 +174,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.Create,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, { name: commonName })
|
||||
);
|
||||
} else {
|
||||
projectId = dto.projectId;
|
||||
@@ -357,8 +357,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.Read,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
|
||||
);
|
||||
|
||||
return expandInternalCa(ca);
|
||||
@@ -383,8 +383,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.Edit,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -416,8 +416,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.Delete,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
|
||||
);
|
||||
|
||||
await certificateAuthorityDAL.deleteById(ca.id);
|
||||
@@ -442,8 +442,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.Create,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
|
||||
);
|
||||
|
||||
if (ca.internalCa.type === InternalCaType.ROOT)
|
||||
@@ -506,8 +506,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.Renew,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
|
||||
);
|
||||
|
||||
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||
@@ -793,8 +793,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.Read,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
|
||||
);
|
||||
|
||||
const caCertChains = await getCaCertChains({
|
||||
@@ -830,8 +830,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.Read,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
|
||||
);
|
||||
|
||||
const { caCert, caCertChain, serialNumber } = await getCaCertChain({
|
||||
@@ -911,8 +911,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.SignIntermediate,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
|
||||
);
|
||||
|
||||
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||
@@ -1059,8 +1059,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
ProjectPermissionCertificateAuthorityActions.Create,
|
||||
subject(ProjectPermissionSub.CertificateAuthorities, { name: ca.name })
|
||||
);
|
||||
|
||||
if (ca.internalCa.parentCaId) {
|
||||
|
||||
@@ -4,6 +4,10 @@ import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import {
|
||||
applyProcessedPermissionRulesToQuery,
|
||||
type ProcessedPermissionRules
|
||||
} from "@app/lib/knex/permission-filter-utils";
|
||||
|
||||
import {
|
||||
EnrollmentType,
|
||||
@@ -276,6 +280,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
issuerType?: IssuerType;
|
||||
caId?: string;
|
||||
} = {},
|
||||
processedRules?: ProcessedPermissionRules,
|
||||
tx?: Knex
|
||||
): Promise<TCertificateProfile[] | TCertificateProfileWithConfigs[]> => {
|
||||
try {
|
||||
@@ -308,7 +313,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.issuerType`, issuerType);
|
||||
}
|
||||
|
||||
const query = baseQuery
|
||||
let query = baseQuery
|
||||
.leftJoin(
|
||||
TableName.CertificateAuthority,
|
||||
`${TableName.PkiCertificateProfile}.caId`,
|
||||
@@ -354,6 +359,14 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeId")
|
||||
);
|
||||
|
||||
if (processedRules) {
|
||||
query = applyProcessedPermissionRulesToQuery(
|
||||
query,
|
||||
TableName.PkiCertificateProfile,
|
||||
processedRules
|
||||
) as typeof query;
|
||||
}
|
||||
|
||||
const results = (await query
|
||||
.orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc")
|
||||
.offset(offset)
|
||||
@@ -432,6 +445,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
issuerType?: IssuerType;
|
||||
caId?: string;
|
||||
} = {},
|
||||
processedRules?: ProcessedPermissionRules,
|
||||
tx?: Knex
|
||||
): Promise<number> => {
|
||||
try {
|
||||
@@ -459,6 +473,14 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
query = query.where({ issuerType });
|
||||
}
|
||||
|
||||
if (processedRules) {
|
||||
query = applyProcessedPermissionRulesToQuery(
|
||||
query,
|
||||
TableName.PkiCertificateProfile,
|
||||
processedRules
|
||||
) as typeof query;
|
||||
}
|
||||
|
||||
const result = await query.count("*").first();
|
||||
return parseInt((result as unknown as { count: string }).count || "0", 10);
|
||||
} catch (error) {
|
||||
|
||||
@@ -169,7 +169,8 @@ describe("CertificateProfileService", () => {
|
||||
const mockPermissionService = {
|
||||
getProjectPermission: vi.fn().mockResolvedValue({
|
||||
permission: {
|
||||
throwUnlessCan: vi.fn()
|
||||
throwUnlessCan: vi.fn(),
|
||||
rules: []
|
||||
}
|
||||
})
|
||||
} as unknown as Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
@@ -598,13 +599,18 @@ describe("CertificateProfileService", () => {
|
||||
|
||||
expect(result.profiles).toEqual(mockProfiles);
|
||||
expect(result.totalCount).toBe(1);
|
||||
expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith("project-123", {
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
search: undefined,
|
||||
enrollmentType: undefined,
|
||||
caId: undefined
|
||||
});
|
||||
expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith(
|
||||
"project-123",
|
||||
{
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
search: undefined,
|
||||
enrollmentType: undefined,
|
||||
caId: undefined,
|
||||
issuerType: undefined
|
||||
},
|
||||
{ allowRules: [], forbidRules: [] }
|
||||
);
|
||||
});
|
||||
|
||||
it("should list profiles with filters", async () => {
|
||||
@@ -618,13 +624,18 @@ describe("CertificateProfileService", () => {
|
||||
caId: "ca-123"
|
||||
});
|
||||
|
||||
expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith("project-123", {
|
||||
offset: 10,
|
||||
limit: 5,
|
||||
search: "test",
|
||||
enrollmentType: EnrollmentType.API,
|
||||
caId: "ca-123"
|
||||
});
|
||||
expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith(
|
||||
"project-123",
|
||||
{
|
||||
offset: 10,
|
||||
limit: 5,
|
||||
search: "test",
|
||||
enrollmentType: EnrollmentType.API,
|
||||
caId: "ca-123",
|
||||
issuerType: undefined
|
||||
},
|
||||
{ allowRules: [], forbidRules: [] }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { buildUrl } from "@app/ee/services/pki-acme/pki-acme-fns";
|
||||
import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils";
|
||||
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
@@ -299,7 +300,9 @@ export const certificateProfileServiceFactory = ({
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Create,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
subject(ProjectPermissionSub.CertificateProfiles, {
|
||||
slug: data.slug
|
||||
})
|
||||
);
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
@@ -454,7 +457,9 @@ export const certificateProfileServiceFactory = ({
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Edit,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
subject(ProjectPermissionSub.CertificateProfiles, {
|
||||
slug: existingProfile.slug
|
||||
})
|
||||
);
|
||||
|
||||
if (data.certificateTemplateId) {
|
||||
@@ -577,7 +582,9 @@ export const certificateProfileServiceFactory = ({
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
subject(ProjectPermissionSub.CertificateProfiles, {
|
||||
slug: profile.slug
|
||||
})
|
||||
);
|
||||
|
||||
const converted = convertDalToService(profile);
|
||||
@@ -613,7 +620,9 @@ export const certificateProfileServiceFactory = ({
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
subject(ProjectPermissionSub.CertificateProfiles, {
|
||||
slug: profile.slug
|
||||
})
|
||||
);
|
||||
|
||||
if (profile.estConfig && profile.estConfig.caChain) {
|
||||
@@ -687,7 +696,9 @@ export const certificateProfileServiceFactory = ({
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
subject(ProjectPermissionSub.CertificateProfiles, {
|
||||
slug
|
||||
})
|
||||
);
|
||||
|
||||
const profile = await certificateProfileDAL.findBySlugAndProjectId(slug, projectId);
|
||||
@@ -739,21 +750,35 @@ export const certificateProfileServiceFactory = ({
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
const profiles = await certificateProfileDAL.findByProjectId(projectId, {
|
||||
offset,
|
||||
limit,
|
||||
search,
|
||||
enrollmentType,
|
||||
issuerType,
|
||||
caId
|
||||
});
|
||||
const processedRules = getProcessedPermissionRules(
|
||||
permission,
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
|
||||
search,
|
||||
enrollmentType,
|
||||
issuerType,
|
||||
caId
|
||||
});
|
||||
const profiles = await certificateProfileDAL.findByProjectId(
|
||||
projectId,
|
||||
{
|
||||
offset,
|
||||
limit,
|
||||
search,
|
||||
enrollmentType,
|
||||
issuerType,
|
||||
caId
|
||||
},
|
||||
processedRules
|
||||
);
|
||||
|
||||
const totalCount = await certificateProfileDAL.countByProjectId(
|
||||
projectId,
|
||||
{
|
||||
search,
|
||||
enrollmentType,
|
||||
issuerType,
|
||||
caId
|
||||
},
|
||||
processedRules
|
||||
);
|
||||
|
||||
const convertedProfiles = await Promise.all(
|
||||
profiles.map(async (profile) => {
|
||||
@@ -838,7 +863,9 @@ export const certificateProfileServiceFactory = ({
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Delete,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
subject(ProjectPermissionSub.CertificateProfiles, {
|
||||
slug: profile.slug
|
||||
})
|
||||
);
|
||||
|
||||
const deletedProfile = await certificateProfileDAL.deleteById(profileId);
|
||||
@@ -884,7 +911,9 @@ export const certificateProfileServiceFactory = ({
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
subject(ProjectPermissionSub.CertificateProfiles, {
|
||||
slug: profile.slug
|
||||
})
|
||||
);
|
||||
|
||||
const certificates = await certificateProfileDAL.getCertificatesByProfile(profileId, {
|
||||
@@ -925,17 +954,9 @@ export const certificateProfileServiceFactory = ({
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||
ProjectPermissionSub.Certificates
|
||||
subject(ProjectPermissionSub.CertificateProfiles, {
|
||||
slug: profile.slug
|
||||
})
|
||||
);
|
||||
|
||||
const cert = await certificateProfileDAL.getLatestActiveCertificateForProfile(profileId);
|
||||
@@ -944,6 +965,24 @@ export const certificateProfileServiceFactory = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
subject(ProjectPermissionSub.Certificates, {
|
||||
commonName: cert.commonName,
|
||||
altNames: cert.altNames ?? undefined,
|
||||
serialNumber: cert.serialNumber
|
||||
})
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||
subject(ProjectPermissionSub.Certificates, {
|
||||
commonName: cert.commonName,
|
||||
altNames: cert.altNames ?? undefined,
|
||||
serialNumber: cert.serialNumber
|
||||
})
|
||||
);
|
||||
|
||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||
|
||||
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||
@@ -1037,7 +1076,9 @@ export const certificateProfileServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
subject(ProjectPermissionSub.CertificateProfiles, {
|
||||
slug: profile.slug
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1088,7 +1129,9 @@ export const certificateProfileServiceFactory = ({
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
subject(ProjectPermissionSub.CertificateProfiles, {
|
||||
slug: profile.slug
|
||||
})
|
||||
);
|
||||
|
||||
if (profile.enrollmentType !== EnrollmentType.ACME) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import {
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCertificateProfileActions,
|
||||
ProjectPermissionSet,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
@@ -79,8 +80,8 @@ describe("CertificateRequestService", () => {
|
||||
const mockPermission = {
|
||||
permission: createMongoAbility<ProjectPermissionSet>([
|
||||
{
|
||||
action: ProjectPermissionCertificateActions.Create,
|
||||
subject: ProjectPermissionSub.Certificates
|
||||
action: ProjectPermissionCertificateProfileActions.IssueCert,
|
||||
subject: ProjectPermissionSub.CertificateProfiles
|
||||
}
|
||||
])
|
||||
};
|
||||
@@ -224,6 +225,10 @@ describe("CertificateRequestService", () => {
|
||||
{
|
||||
action: ProjectPermissionCertificateActions.Read,
|
||||
subject: ProjectPermissionSub.Certificates
|
||||
},
|
||||
{
|
||||
action: ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||
subject: ProjectPermissionSub.Certificates
|
||||
}
|
||||
])
|
||||
};
|
||||
@@ -318,7 +323,7 @@ describe("CertificateRequestService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should get certificate from request successfully when private key access is denied", async () => {
|
||||
it("should get certificate from request successfully when user lacks private key permission", async () => {
|
||||
const mockPermission = {
|
||||
permission: createMongoAbility<ProjectPermissionSet>([
|
||||
{
|
||||
@@ -348,7 +353,6 @@ describe("CertificateRequestService", () => {
|
||||
(mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission);
|
||||
(mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithCert);
|
||||
(mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody);
|
||||
(mockCertificateService.getCertPrivateKey as any).mockRejectedValue(new Error("Private key access denied"));
|
||||
|
||||
const result = await service.getCertificateFromRequest(mockGetData);
|
||||
|
||||
@@ -362,8 +366,68 @@ describe("CertificateRequestService", () => {
|
||||
actorAuthMethod: AuthMethod.EMAIL,
|
||||
actorOrgId: "550e8400-e29b-41d4-a716-446655440002"
|
||||
});
|
||||
expect(mockCertificateService.getCertPrivateKey).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
status: CertificateRequestStatus.ISSUED,
|
||||
certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----",
|
||||
privateKey: null,
|
||||
serialNumber: "123456",
|
||||
errorMessage: null,
|
||||
createdAt: mockRequestWithCert.createdAt,
|
||||
updatedAt: mockRequestWithCert.updatedAt
|
||||
});
|
||||
});
|
||||
|
||||
it("should get certificate from request successfully when user has private key permission but key retrieval fails", async () => {
|
||||
const mockPermission = {
|
||||
permission: createMongoAbility<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({
|
||||
id: "550e8400-e29b-41d4-a716-446655440008",
|
||||
id: "550e8400-e29b-41d4-a716-446655440009",
|
||||
actor: ActorType.USER,
|
||||
actorId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
actorAuthMethod: AuthMethod.EMAIL,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import {
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCertificateProfileActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
@@ -110,8 +111,8 @@ export const certificateRequestServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.Create,
|
||||
ProjectPermissionSub.Certificates
|
||||
ProjectPermissionCertificateProfileActions.IssueCert,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
}
|
||||
|
||||
@@ -217,20 +218,25 @@ export const certificateRequestServiceFactory = ({
|
||||
actorOrgId
|
||||
});
|
||||
|
||||
// Try to get private key (may fail if user doesn't have permission)
|
||||
const canReadPrivateKey = permission.can(
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
let privateKey: string | null = null;
|
||||
try {
|
||||
const certPrivateKey = await certificateService.getCertPrivateKey({
|
||||
id: certificateRequest.certificate.id,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
privateKey = certPrivateKey.certPrivateKey;
|
||||
} catch (error) {
|
||||
// Private key access denied - continue without it
|
||||
privateKey = null;
|
||||
if (canReadPrivateKey) {
|
||||
try {
|
||||
const certPrivateKey = await certificateService.getCertPrivateKey({
|
||||
id: certificateRequest.certificate.id,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
privateKey = certPrivateKey.certPrivateKey;
|
||||
} catch (error) {
|
||||
privateKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,6 +5,10 @@ import { TableName } from "@app/db/schemas";
|
||||
import { TPkiCertificateTemplatesV2Insert } from "@app/db/schemas/pki-certificate-templates-v2";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
import {
|
||||
applyProcessedPermissionRulesToQuery,
|
||||
type ProcessedPermissionRules
|
||||
} from "@app/lib/knex/permission-filter-utils";
|
||||
|
||||
import {
|
||||
TCertificateTemplateV2,
|
||||
@@ -133,6 +137,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
||||
limit?: number;
|
||||
search?: string;
|
||||
} = {},
|
||||
processedRules?: ProcessedPermissionRules,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
@@ -146,6 +151,14 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (processedRules) {
|
||||
query = applyProcessedPermissionRulesToQuery(
|
||||
query,
|
||||
TableName.PkiCertificateTemplateV2,
|
||||
processedRules
|
||||
) as typeof query;
|
||||
}
|
||||
|
||||
const certificateTemplatesV2 = await query.orderBy("createdAt", "desc").offset(offset).limit(limit);
|
||||
|
||||
return certificateTemplatesV2.map((template: Record<string, unknown>) => parseJsonFields(template));
|
||||
@@ -159,6 +172,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
||||
options: {
|
||||
search?: string;
|
||||
} = {},
|
||||
processedRules?: ProcessedPermissionRules,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
@@ -172,6 +186,14 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (processedRules) {
|
||||
query = applyProcessedPermissionRulesToQuery(
|
||||
query,
|
||||
TableName.PkiCertificateTemplateV2,
|
||||
processedRules
|
||||
) as typeof query;
|
||||
}
|
||||
|
||||
const result = await query.count("*").first();
|
||||
return parseInt((result as unknown as { count: string }).count || "0", 10);
|
||||
} catch (error) {
|
||||
|
||||
+32
-16
@@ -267,14 +267,22 @@ describe("CertificateTemplateV2Service", () => {
|
||||
limit: 20
|
||||
});
|
||||
|
||||
expect(mockCertificateTemplateV2DAL.findByProjectId).toHaveBeenCalledWith("project-123", {
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
search: undefined
|
||||
});
|
||||
expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith("project-123", {
|
||||
search: undefined
|
||||
});
|
||||
expect(mockCertificateTemplateV2DAL.findByProjectId).toHaveBeenCalledWith(
|
||||
"project-123",
|
||||
{
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
search: undefined
|
||||
},
|
||||
{ allowRules: [], forbidRules: [] }
|
||||
);
|
||||
expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith(
|
||||
"project-123",
|
||||
{
|
||||
search: undefined
|
||||
},
|
||||
{ allowRules: [], forbidRules: [] }
|
||||
);
|
||||
expect(result).toEqual({ templates, totalCount });
|
||||
});
|
||||
|
||||
@@ -291,14 +299,22 @@ describe("CertificateTemplateV2Service", () => {
|
||||
search: "web server"
|
||||
});
|
||||
|
||||
expect(mockCertificateTemplateV2DAL.findByProjectId).toHaveBeenCalledWith("project-123", {
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
search: "web server"
|
||||
});
|
||||
expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith("project-123", {
|
||||
search: "web server"
|
||||
});
|
||||
expect(mockCertificateTemplateV2DAL.findByProjectId).toHaveBeenCalledWith(
|
||||
"project-123",
|
||||
{
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
search: "web server"
|
||||
},
|
||||
{ allowRules: [], forbidRules: [] }
|
||||
);
|
||||
expect(mockCertificateTemplateV2DAL.countByProjectId).toHaveBeenCalledWith(
|
||||
"project-123",
|
||||
{
|
||||
search: "web server"
|
||||
},
|
||||
{ allowRules: [], forbidRules: [] }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import RE2 from "re2";
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils";
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
@@ -77,12 +78,12 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
};
|
||||
|
||||
const validateSubjectAttributePolicy = (
|
||||
subject: Array<{ type: string; allowed?: string[]; required?: string[]; denied?: string[] }>
|
||||
subjectAttributes: Array<{ type: string; allowed?: string[]; required?: string[]; denied?: string[] }>
|
||||
) => {
|
||||
if (!subject || subject.length === 0) return;
|
||||
if (!subjectAttributes || subjectAttributes.length === 0) return;
|
||||
|
||||
// Validate each subject attribute policy
|
||||
for (const attr of subject) {
|
||||
for (const attr of subjectAttributes) {
|
||||
// Ensure at least one field is provided
|
||||
if (!attr.allowed && !attr.required && !attr.denied) {
|
||||
throw new ForbiddenRequestError({
|
||||
@@ -634,7 +635,9 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiTemplateActions.Create,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
subject(ProjectPermissionSub.CertificateTemplates, {
|
||||
name: data.name
|
||||
})
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
@@ -711,7 +714,9 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiTemplateActions.Edit,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
subject(ProjectPermissionSub.CertificateTemplates, {
|
||||
name: existingTemplate.name
|
||||
})
|
||||
);
|
||||
|
||||
const consolidatedData = {
|
||||
@@ -784,7 +789,9 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiTemplateActions.Read,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
subject(ProjectPermissionSub.CertificateTemplates, {
|
||||
name: template.name
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -815,16 +822,17 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiTemplateActions.Read,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
|
||||
const template = await certificateTemplateV2DAL.findByNameAndProjectId(slug, projectId);
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Certificate template not found" });
|
||||
}
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiTemplateActions.Read,
|
||||
subject(ProjectPermissionSub.CertificateTemplates, {
|
||||
name: template.name
|
||||
})
|
||||
);
|
||||
return template;
|
||||
};
|
||||
|
||||
@@ -864,13 +872,18 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
|
||||
const templates = await certificateTemplateV2DAL.findByProjectId(projectId, {
|
||||
offset,
|
||||
limit,
|
||||
search
|
||||
});
|
||||
const processedRules = getProcessedPermissionRules(
|
||||
permission,
|
||||
ProjectPermissionPkiTemplateActions.Read,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
const templates = await certificateTemplateV2DAL.findByProjectId(
|
||||
projectId,
|
||||
{ offset, limit, search },
|
||||
processedRules
|
||||
);
|
||||
|
||||
const totalCount = await certificateTemplateV2DAL.countByProjectId(projectId, { search });
|
||||
const totalCount = await certificateTemplateV2DAL.countByProjectId(projectId, { search }, processedRules);
|
||||
|
||||
return {
|
||||
templates,
|
||||
@@ -907,7 +920,9 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiTemplateActions.Delete,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
subject(ProjectPermissionSub.CertificateTemplates, {
|
||||
name: template.name
|
||||
})
|
||||
);
|
||||
|
||||
const isInUse = await certificateTemplateV2DAL.isTemplateInUse(templateId);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import { randomUUID } from "crypto";
|
||||
import RE2 from "re2";
|
||||
@@ -167,7 +167,9 @@ const validateProfileAndPermissions = async (
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.IssueCert,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
subject(ProjectPermissionSub.CertificateProfiles, {
|
||||
slug: profile.slug
|
||||
})
|
||||
);
|
||||
|
||||
return profile;
|
||||
@@ -999,11 +1001,27 @@ export const certificateV3ServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: profile.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
const canReadPrivateKey = permission.can(
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
const privateKeyForResponse = canReadPrivateKey ? selfSignedResult.privateKey.toString("utf8") : undefined;
|
||||
|
||||
return {
|
||||
certificate: selfSignedResult.certificate.toString("utf8"),
|
||||
issuingCaCertificate: "",
|
||||
certificateChain: selfSignedResult.certificate.toString("utf8"),
|
||||
privateKey: selfSignedResult.privateKey.toString("utf8"),
|
||||
privateKey: privateKeyForResponse,
|
||||
serialNumber: selfSignedResult.serialNumber,
|
||||
certificateId: certificateData.id,
|
||||
certificateRequestId,
|
||||
@@ -1100,11 +1118,28 @@ export const certificateV3ServiceFactory = ({
|
||||
finalCertificateChain = removeRootCaFromChain(finalCertificateChain);
|
||||
}
|
||||
|
||||
// Check if user has permission to read private key
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: profile.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
const canReadPrivateKey = permission.can(
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
const privateKeyForResponse = canReadPrivateKey ? bufferToString(privateKey) : undefined;
|
||||
|
||||
return {
|
||||
certificate: bufferToString(certificate),
|
||||
issuingCaCertificate: bufferToString(issuingCaCertificate),
|
||||
certificateChain: finalCertificateChain,
|
||||
privateKey: bufferToString(privateKey),
|
||||
privateKey: privateKeyForResponse,
|
||||
serialNumber,
|
||||
certificateId: cert.id,
|
||||
certificateRequestId,
|
||||
@@ -1489,10 +1524,12 @@ export const certificateV3ServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.IssueCert,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
if (profile) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.IssueCert,
|
||||
subject(ProjectPermissionSub.CertificateProfiles, { slug: profile.slug })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const issuerType = profile?.issuerType || (originalCert.caId ? IssuerType.CA : IssuerType.SELF_SIGNED);
|
||||
@@ -1869,7 +1906,11 @@ export const certificateV3ServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.Edit,
|
||||
ProjectPermissionSub.Certificates
|
||||
subject(ProjectPermissionSub.Certificates, {
|
||||
commonName: certificate.commonName,
|
||||
altNames: certificate.altNames ?? undefined,
|
||||
serialNumber: certificate.serialNumber
|
||||
})
|
||||
);
|
||||
|
||||
if (!certificate.profileId) {
|
||||
@@ -1972,7 +2013,11 @@ export const certificateV3ServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.Edit,
|
||||
ProjectPermissionSub.Certificates
|
||||
subject(ProjectPermissionSub.Certificates, {
|
||||
commonName: certificate.commonName,
|
||||
altNames: certificate.altNames ?? undefined,
|
||||
serialNumber: certificate.serialNumber
|
||||
})
|
||||
);
|
||||
|
||||
if (!certificate.profileId) {
|
||||
|
||||
@@ -4,6 +4,10 @@ import { TDbClient } from "@app/db";
|
||||
import { TableName, TCertificates } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import {
|
||||
applyProcessedPermissionRulesToQuery,
|
||||
type ProcessedPermissionRules
|
||||
} from "@app/lib/knex/permission-filter-utils";
|
||||
|
||||
import { CertStatus } from "./certificate-types";
|
||||
|
||||
@@ -140,7 +144,8 @@ export const certificateDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findActiveCertificatesForSync = async (
|
||||
filter: Partial<TCertificates & { friendlyName?: string; commonName?: string }>,
|
||||
options?: { limit?: number; offset?: number }
|
||||
options?: { limit?: number; offset?: number },
|
||||
permissionFilters?: ProcessedPermissionRules
|
||||
): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => {
|
||||
try {
|
||||
let query = db
|
||||
@@ -163,6 +168,10 @@ export const certificateDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
});
|
||||
|
||||
if (permissionFilters) {
|
||||
query = applyProcessedPermissionRulesToQuery(query, TableName.Certificate, permissionFilters) as typeof query;
|
||||
}
|
||||
|
||||
if (options?.offset) {
|
||||
query = query.offset(options.offset);
|
||||
}
|
||||
@@ -267,7 +276,8 @@ export const certificateDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findWithPrivateKeyInfo = async (
|
||||
filter: Partial<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 })[]> => {
|
||||
try {
|
||||
let query = db
|
||||
@@ -287,6 +297,10 @@ export const certificateDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
});
|
||||
|
||||
if (permissionFilters) {
|
||||
query = applyProcessedPermissionRulesToQuery(query, TableName.Certificate, permissionFilters) as typeof query;
|
||||
}
|
||||
|
||||
if (options?.offset) {
|
||||
query = query.offset(options.offset);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
@@ -108,7 +108,11 @@ export const certificateServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionSub.Certificates
|
||||
subject(ProjectPermissionSub.Certificates, {
|
||||
commonName: cert.commonName,
|
||||
altNames: cert.altNames ?? undefined,
|
||||
serialNumber: cert.serialNumber
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -140,7 +144,11 @@ export const certificateServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||
ProjectPermissionSub.Certificates
|
||||
subject(ProjectPermissionSub.Certificates, {
|
||||
commonName: cert.commonName,
|
||||
altNames: cert.altNames ?? undefined,
|
||||
serialNumber: cert.serialNumber
|
||||
})
|
||||
);
|
||||
|
||||
const { certPrivateKey } = await getCertificateCredentials({
|
||||
@@ -174,7 +182,11 @@ export const certificateServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.Delete,
|
||||
ProjectPermissionSub.Certificates
|
||||
subject(ProjectPermissionSub.Certificates, {
|
||||
commonName: cert.commonName,
|
||||
altNames: cert.altNames ?? undefined,
|
||||
serialNumber: cert.serialNumber
|
||||
})
|
||||
);
|
||||
|
||||
const deletedCert = await certificateDAL.deleteById(cert.id);
|
||||
@@ -234,7 +246,13 @@ export const certificateServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.Delete,
|
||||
ProjectPermissionSub.Certificates
|
||||
subject(ProjectPermissionSub.Certificates, {
|
||||
commonName: cert.commonName,
|
||||
altNames: cert.altNames ?? undefined,
|
||||
serialNumber: cert.serialNumber,
|
||||
friendlyName: cert.friendlyName,
|
||||
status: cert.status
|
||||
})
|
||||
);
|
||||
|
||||
if (cert.status === CertStatus.REVOKED) throw new Error("Certificate already revoked");
|
||||
@@ -309,7 +327,11 @@ export const certificateServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionSub.Certificates
|
||||
subject(ProjectPermissionSub.Certificates, {
|
||||
commonName: cert.commonName,
|
||||
altNames: cert.altNames ?? undefined,
|
||||
serialNumber: cert.serialNumber
|
||||
})
|
||||
);
|
||||
|
||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||
@@ -397,7 +419,7 @@ export const certificateServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.Create,
|
||||
ProjectPermissionCertificateActions.Import,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
@@ -610,11 +632,23 @@ export const certificateServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionSub.Certificates
|
||||
subject(ProjectPermissionSub.Certificates, {
|
||||
commonName: cert.commonName,
|
||||
altNames: cert.altNames ?? undefined,
|
||||
serialNumber: cert.serialNumber,
|
||||
friendlyName: cert.friendlyName,
|
||||
status: cert.status
|
||||
})
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||
ProjectPermissionSub.Certificates
|
||||
subject(ProjectPermissionSub.Certificates, {
|
||||
commonName: cert.commonName,
|
||||
altNames: cert.altNames ?? undefined,
|
||||
serialNumber: cert.serialNumber,
|
||||
friendlyName: cert.friendlyName,
|
||||
status: cert.status
|
||||
})
|
||||
);
|
||||
|
||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||
@@ -726,7 +760,13 @@ export const certificateServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||
ProjectPermissionSub.Certificates
|
||||
subject(ProjectPermissionSub.Certificates, {
|
||||
commonName: cert.commonName,
|
||||
altNames: cert.altNames ?? undefined,
|
||||
serialNumber: cert.serialNumber,
|
||||
friendlyName: cert.friendlyName,
|
||||
status: cert.status
|
||||
})
|
||||
);
|
||||
|
||||
// Get certificate bundle (certificate, chain, private key)
|
||||
|
||||
@@ -4,6 +4,10 @@ import { TDbClient } from "@app/db";
|
||||
import { TableName, TPkiSyncs } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex";
|
||||
import {
|
||||
applyProcessedPermissionRulesToQuery,
|
||||
type ProcessedPermissionRules
|
||||
} from "@app/lib/knex/permission-filter-utils";
|
||||
|
||||
import { PkiSync } from "./pki-sync-enums";
|
||||
|
||||
@@ -45,13 +49,15 @@ const basePkiSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: PkiSyncF
|
||||
const basePkiSyncWithSubscriberQuery = ({
|
||||
filter,
|
||||
db,
|
||||
tx
|
||||
tx,
|
||||
processedRules
|
||||
}: {
|
||||
db: TDbClient;
|
||||
filter?: PkiSyncFindFilter;
|
||||
tx?: Knex;
|
||||
processedRules?: ProcessedPermissionRules;
|
||||
}) => {
|
||||
const query = (tx || db.replicaNode())(TableName.PkiSync)
|
||||
let query = (tx || db.replicaNode())(TableName.PkiSync)
|
||||
.leftJoin(TableName.AppConnection, `${TableName.PkiSync}.connectionId`, `${TableName.AppConnection}.id`)
|
||||
.leftJoin(TableName.PkiSubscriber, `${TableName.PkiSync}.subscriberId`, `${TableName.PkiSubscriber}.id`)
|
||||
.select(selectAllTableCols(TableName.PkiSync))
|
||||
@@ -82,6 +88,10 @@ const basePkiSyncWithSubscriberQuery = ({
|
||||
void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.PkiSync, filter)));
|
||||
}
|
||||
|
||||
if (processedRules) {
|
||||
query = applyProcessedPermissionRulesToQuery(query, TableName.PkiSync, processedRules) as typeof query;
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
@@ -184,9 +194,18 @@ export const pkiSyncDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findByProjectIdWithSubscribers = async (projectId: string, tx?: Knex) => {
|
||||
const findByProjectIdWithSubscribers = async (
|
||||
projectId: string,
|
||||
processedRules?: ProcessedPermissionRules,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const pkiSyncs = await basePkiSyncWithSubscriberQuery({ filter: { projectId }, db, tx });
|
||||
const pkiSyncs = await basePkiSyncWithSubscriberQuery({
|
||||
filter: { projectId },
|
||||
db,
|
||||
tx,
|
||||
processedRules
|
||||
});
|
||||
return pkiSyncs.map(expandPkiSyncWithSubscriber);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find By Project ID With Subscribers - PKI Sync" });
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ActionProjectType, TCertificateSyncs } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { ProjectPermissionPkiSyncActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils";
|
||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
@@ -145,9 +146,10 @@ export const pkiSyncServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSyncActions.Create,
|
||||
subscriber
|
||||
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: subscriber.name })
|
||||
: ProjectPermissionSub.PkiSyncs
|
||||
subject(ProjectPermissionSub.PkiSyncs, {
|
||||
subscriberName: subscriber?.name,
|
||||
name
|
||||
})
|
||||
);
|
||||
|
||||
// Get the destination app type based on PKI sync destination
|
||||
@@ -235,9 +237,10 @@ export const pkiSyncServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSyncActions.Edit,
|
||||
currentSubscriber
|
||||
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: currentSubscriber.name })
|
||||
: ProjectPermissionSub.PkiSyncs
|
||||
subject(ProjectPermissionSub.PkiSyncs, {
|
||||
subscriberName: currentSubscriber?.name,
|
||||
name: pkiSync.name
|
||||
})
|
||||
);
|
||||
|
||||
if (name && name !== pkiSync.name) {
|
||||
@@ -331,9 +334,10 @@ export const pkiSyncServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSyncActions.Delete,
|
||||
pkiSyncSubscriber
|
||||
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: pkiSyncSubscriber.name })
|
||||
: ProjectPermissionSub.PkiSyncs
|
||||
subject(ProjectPermissionSub.PkiSyncs, {
|
||||
subscriberName: pkiSyncSubscriber?.name,
|
||||
name: pkiSync.name
|
||||
})
|
||||
);
|
||||
|
||||
return pkiSyncDAL.deleteById(id);
|
||||
@@ -354,7 +358,13 @@ export const pkiSyncServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs);
|
||||
|
||||
const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId);
|
||||
const processedRules = getProcessedPermissionRules(
|
||||
permission,
|
||||
ProjectPermissionPkiSyncActions.Read,
|
||||
ProjectPermissionSub.PkiSyncs
|
||||
);
|
||||
|
||||
const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId, processedRules);
|
||||
|
||||
if (certificateId) {
|
||||
const syncsWithCertificateInfo = await Promise.all(
|
||||
@@ -406,9 +416,10 @@ export const pkiSyncServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSyncActions.Read,
|
||||
findSubscriber
|
||||
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: findSubscriber.name })
|
||||
: ProjectPermissionSub.PkiSyncs
|
||||
subject(ProjectPermissionSub.PkiSyncs, {
|
||||
subscriberName: findSubscriber?.name,
|
||||
name: pkiSync.name
|
||||
})
|
||||
);
|
||||
|
||||
const result = {
|
||||
@@ -442,9 +453,10 @@ export const pkiSyncServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSyncActions.SyncCertificates,
|
||||
syncSubscriber
|
||||
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: syncSubscriber.name })
|
||||
: ProjectPermissionSub.PkiSyncs
|
||||
subject(ProjectPermissionSub.PkiSyncs, {
|
||||
subscriberName: syncSubscriber?.name,
|
||||
name: pkiSync.name
|
||||
})
|
||||
);
|
||||
|
||||
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: id });
|
||||
@@ -483,9 +495,10 @@ export const pkiSyncServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSyncActions.ImportCertificates,
|
||||
importSubscriber
|
||||
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: importSubscriber.name })
|
||||
: ProjectPermissionSub.PkiSyncs
|
||||
subject(ProjectPermissionSub.PkiSyncs, {
|
||||
subscriberName: importSubscriber?.name,
|
||||
name: pkiSync.name
|
||||
})
|
||||
);
|
||||
|
||||
await pkiSyncQueue.queuePkiSyncImportCertificatesById({ syncId: id });
|
||||
@@ -516,9 +529,10 @@ export const pkiSyncServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSyncActions.RemoveCertificates,
|
||||
removeSubscriber
|
||||
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: removeSubscriber.name })
|
||||
: ProjectPermissionSub.PkiSyncs
|
||||
subject(ProjectPermissionSub.PkiSyncs, {
|
||||
subscriberName: removeSubscriber?.name,
|
||||
name: pkiSync.name
|
||||
})
|
||||
);
|
||||
|
||||
await pkiSyncQueue.queuePkiSyncRemoveCertificatesById({ syncId: id });
|
||||
@@ -549,7 +563,18 @@ export const pkiSyncServiceFactory = ({
|
||||
projectId: pkiSync.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Edit, ProjectPermissionSub.PkiSyncs);
|
||||
let pkiSyncSubscriber;
|
||||
if (pkiSync.subscriberId) {
|
||||
pkiSyncSubscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId);
|
||||
}
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSyncActions.Edit,
|
||||
subject(ProjectPermissionSub.PkiSyncs, {
|
||||
subscriberName: pkiSyncSubscriber?.name,
|
||||
name: pkiSync.name
|
||||
})
|
||||
);
|
||||
|
||||
await validateCertificatesProjectOwnership(certificateIds, pkiSync.projectId);
|
||||
|
||||
@@ -588,7 +613,12 @@ export const pkiSyncServiceFactory = ({
|
||||
projectId: pkiSync.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Edit, ProjectPermissionSub.PkiSyncs);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSyncActions.Edit,
|
||||
subject(ProjectPermissionSub.PkiSyncs, {
|
||||
name: pkiSync.name
|
||||
})
|
||||
);
|
||||
|
||||
const removedCount = await certificateSyncDAL.removeCertificates(pkiSyncId, certificateIds);
|
||||
|
||||
@@ -626,7 +656,12 @@ export const pkiSyncServiceFactory = ({
|
||||
projectId: pkiSync.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSyncActions.Read,
|
||||
subject(ProjectPermissionSub.PkiSyncs, {
|
||||
name: pkiSync.name
|
||||
})
|
||||
);
|
||||
|
||||
const result = await certificateSyncDAL.findWithDetails({
|
||||
pkiSyncId,
|
||||
|
||||
@@ -20,6 +20,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCertificateAuthorityActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
@@ -39,6 +40,7 @@ import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certific
|
||||
import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
|
||||
import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal";
|
||||
import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getProcessedPermissionRules } from "@app/lib/casl/permission-filter-utils";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
@@ -911,7 +913,7 @@ export const projectServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionCertificateAuthorityActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
@@ -963,35 +965,38 @@ export const projectServiceFactory = ({
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
const regularFilters = {
|
||||
projectId,
|
||||
...(friendlyName && { friendlyName }),
|
||||
...(commonName && { commonName })
|
||||
};
|
||||
const permissionFilters = getProcessedPermissionRules(
|
||||
permission,
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
const certificates = forPkiSync
|
||||
? await certificateDAL.findActiveCertificatesForSync(
|
||||
{
|
||||
projectId,
|
||||
...(friendlyName && { friendlyName }),
|
||||
...(commonName && { commonName })
|
||||
},
|
||||
{ offset, limit }
|
||||
)
|
||||
? await certificateDAL.findActiveCertificatesForSync(regularFilters, { offset, limit }, permissionFilters)
|
||||
: await certificateDAL.findWithPrivateKeyInfo(
|
||||
regularFilters,
|
||||
{
|
||||
projectId,
|
||||
...(friendlyName && { friendlyName }),
|
||||
...(commonName && { commonName })
|
||||
offset,
|
||||
limit,
|
||||
sort: [["notAfter", "desc"]]
|
||||
},
|
||||
{ offset, limit, sort: [["notAfter", "desc"]] }
|
||||
permissionFilters
|
||||
);
|
||||
|
||||
const countFilter = {
|
||||
projectId,
|
||||
...(regularFilters.friendlyName && { friendlyName: String(regularFilters.friendlyName) }),
|
||||
...(regularFilters.commonName && { commonName: String(regularFilters.commonName) })
|
||||
};
|
||||
|
||||
const count = forPkiSync
|
||||
? await certificateDAL.countActiveCertificatesForSync({
|
||||
projectId,
|
||||
friendlyName,
|
||||
commonName
|
||||
})
|
||||
: await certificateDAL.countCertificatesInProject({
|
||||
projectId,
|
||||
friendlyName,
|
||||
commonName
|
||||
});
|
||||
? await certificateDAL.countActiveCertificatesForSync(countFilter)
|
||||
: await certificateDAL.countCertificatesInProject(countFilter);
|
||||
|
||||
return {
|
||||
certificates,
|
||||
|
||||
Reference in New Issue
Block a user