Merge pull request #3628 from Infisical/ENG-2800

feat(policies): Specific permission for bypassing policy
This commit is contained in:
x032205
2025-05-21 14:48:36 -04:00
committed by GitHub
14 changed files with 188 additions and 113 deletions
@@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { ProjectPermissionApprovalActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
@@ -98,7 +98,7 @@ export const accessApprovalPolicyServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionApprovalActions.Create,
ProjectPermissionSub.SecretApproval
);
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
@@ -256,7 +256,10 @@ export const accessApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionApprovalActions.Edit,
ProjectPermissionSub.SecretApproval
);
const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.updateById(
@@ -341,7 +344,7 @@ export const accessApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionApprovalActions.Delete,
ProjectPermissionSub.SecretApproval
);
@@ -432,7 +435,10 @@ export const accessApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionApprovalActions.Read,
ProjectPermissionSub.SecretApproval
);
return policy;
};
@@ -2,6 +2,7 @@ import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability"
import {
ProjectPermissionActions,
ProjectPermissionApprovalActions,
ProjectPermissionCertificateActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
@@ -25,7 +26,6 @@ const buildAdminPermissionRules = () => {
[
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.SecretImports,
ProjectPermissionSub.SecretApproval,
ProjectPermissionSub.Role,
ProjectPermissionSub.Integrations,
ProjectPermissionSub.Webhooks,
@@ -55,6 +55,17 @@ const buildAdminPermissionRules = () => {
);
});
can(
[
ProjectPermissionApprovalActions.Read,
ProjectPermissionApprovalActions.Edit,
ProjectPermissionApprovalActions.Create,
ProjectPermissionApprovalActions.Delete,
ProjectPermissionApprovalActions.AllowChangeBypass
],
ProjectPermissionSub.SecretApproval
);
can(
[
ProjectPermissionCertificateActions.Read,
@@ -243,7 +254,7 @@ const buildMemberPermissionRules = () => {
ProjectPermissionSub.SecretImports
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
can([ProjectPermissionApprovalActions.Read], ProjectPermissionSub.SecretApproval);
can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation);
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
@@ -391,7 +402,7 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionApprovalActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
@@ -34,6 +34,14 @@ export enum ProjectPermissionSecretActions {
Delete = "delete"
}
export enum ProjectPermissionApprovalActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
AllowChangeBypass = "allow-change-bypass"
}
export enum ProjectPermissionCmekActions {
Read = "read",
Create = "create",
@@ -242,7 +250,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionApprovalActions, ProjectPermissionSub.SecretApproval]
| [
ProjectPermissionSecretRotationActions,
(
@@ -439,7 +447,7 @@ const PkiSubscriberConditionSchema = z
const GeneralPermissionSchema = [
z.object({
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionApprovalActions).describe(
"Describe what action an entity can take."
)
}),
@@ -605,7 +613,7 @@ const GeneralPermissionSchema = [
})
];
// Do not update this schema anymore, as it's kept purely for backwards compatability. Update V2 schema only.
// Do not update this schema anymore, as it's kept purely for backwards compatibility. Update V2 schema only.
export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
z.object({
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
@@ -3,7 +3,7 @@ import picomatch from "picomatch";
import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { ProjectPermissionApprovalActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { containsGlobPatterns } from "@app/lib/picomatch";
@@ -89,7 +89,7 @@ export const secretApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionApprovalActions.Create,
ProjectPermissionSub.SecretApproval
);
@@ -204,7 +204,10 @@ export const secretApprovalPolicyServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionApprovalActions.Edit,
ProjectPermissionSub.SecretApproval
);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) {
@@ -301,7 +304,7 @@ export const secretApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionApprovalActions.Delete,
ProjectPermissionSub.SecretApproval
);
@@ -340,7 +343,10 @@ export const secretApprovalPolicyServiceFactory = ({
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionApprovalActions.Read,
ProjectPermissionSub.SecretApproval
);
const sapPolicies = await secretApprovalPolicyDAL.find({ projectId, deletedAt: null });
return sapPolicies;
@@ -413,7 +419,10 @@ export const secretApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionApprovalActions.Read,
ProjectPermissionSub.SecretApproval
);
return sapPolicy;
};
@@ -62,7 +62,11 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission";
import {
ProjectPermissionApprovalActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "../permission/project-permission";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
@@ -504,7 +508,7 @@ export const secretApprovalRequestServiceFactory = ({
});
}
const { hasRole } = await permissionService.getProjectPermission({
const { hasRole, permission } = await permissionService.getProjectPermission({
actor: ActorType.USER,
actorId,
projectId,
@@ -531,7 +535,13 @@ export const secretApprovalRequestServiceFactory = ({
).length;
const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft;
if (!hasMinApproval && !isSoftEnforcement)
if (
!hasMinApproval &&
!(
isSoftEnforcement &&
permission.can(ProjectPermissionApprovalActions.AllowChangeBypass, ProjectPermissionSub.SecretApproval)
)
)
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
const { botKey, shouldUseSecretV2Bridge, project } = await projectBotService.getBotKey(projectId);