feat(policies): Bypass Approval Rework

This commit is contained in:
x032205
2025-05-27 20:46:46 -04:00
parent e69354b546
commit 3190ff2eb1
41 changed files with 1109 additions and 251 deletions
+17
View File
@@ -6,6 +6,9 @@ import {
TAccessApprovalPoliciesApprovers, TAccessApprovalPoliciesApprovers,
TAccessApprovalPoliciesApproversInsert, TAccessApprovalPoliciesApproversInsert,
TAccessApprovalPoliciesApproversUpdate, TAccessApprovalPoliciesApproversUpdate,
TAccessApprovalPoliciesBypassers,
TAccessApprovalPoliciesBypassersInsert,
TAccessApprovalPoliciesBypassersUpdate,
TAccessApprovalPoliciesInsert, TAccessApprovalPoliciesInsert,
TAccessApprovalPoliciesUpdate, TAccessApprovalPoliciesUpdate,
TAccessApprovalRequests, TAccessApprovalRequests,
@@ -276,6 +279,9 @@ import {
TSecretApprovalPoliciesApprovers, TSecretApprovalPoliciesApprovers,
TSecretApprovalPoliciesApproversInsert, TSecretApprovalPoliciesApproversInsert,
TSecretApprovalPoliciesApproversUpdate, TSecretApprovalPoliciesApproversUpdate,
TSecretApprovalPoliciesBypassers,
TSecretApprovalPoliciesBypassersInsert,
TSecretApprovalPoliciesBypassersUpdate,
TSecretApprovalPoliciesInsert, TSecretApprovalPoliciesInsert,
TSecretApprovalPoliciesUpdate, TSecretApprovalPoliciesUpdate,
TSecretApprovalRequests, TSecretApprovalRequests,
@@ -820,6 +826,12 @@ declare module "knex/types/tables" {
TAccessApprovalPoliciesApproversUpdate TAccessApprovalPoliciesApproversUpdate
>; >;
[TableName.AccessApprovalPolicyBypasser]: KnexOriginal.CompositeTableType<
TAccessApprovalPoliciesBypassers,
TAccessApprovalPoliciesBypassersInsert,
TAccessApprovalPoliciesBypassersUpdate
>;
[TableName.AccessApprovalRequest]: KnexOriginal.CompositeTableType< [TableName.AccessApprovalRequest]: KnexOriginal.CompositeTableType<
TAccessApprovalRequests, TAccessApprovalRequests,
TAccessApprovalRequestsInsert, TAccessApprovalRequestsInsert,
@@ -843,6 +855,11 @@ declare module "knex/types/tables" {
TSecretApprovalPoliciesApproversInsert, TSecretApprovalPoliciesApproversInsert,
TSecretApprovalPoliciesApproversUpdate TSecretApprovalPoliciesApproversUpdate
>; >;
[TableName.SecretApprovalPolicyBypasser]: KnexOriginal.CompositeTableType<
TSecretApprovalPoliciesBypassers,
TSecretApprovalPoliciesBypassersInsert,
TSecretApprovalPoliciesBypassersUpdate
>;
[TableName.SecretApprovalRequest]: KnexOriginal.CompositeTableType< [TableName.SecretApprovalRequest]: KnexOriginal.CompositeTableType<
TSecretApprovalRequests, TSecretApprovalRequests,
TSecretApprovalRequestsInsert, TSecretApprovalRequestsInsert,
@@ -0,0 +1,48 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.AccessApprovalPolicyBypasser))) {
await knex.schema.createTable(TableName.AccessApprovalPolicyBypasser, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("bypasserGroupId").nullable();
t.foreign("bypasserGroupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
t.uuid("bypasserUserId").nullable();
t.foreign("bypasserUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.uuid("policyId").notNullable();
t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE");
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyBypasser);
}
if (!(await knex.schema.hasTable(TableName.SecretApprovalPolicyBypasser))) {
await knex.schema.createTable(TableName.SecretApprovalPolicyBypasser, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("bypasserGroupId").nullable();
t.foreign("bypasserGroupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
t.uuid("bypasserUserId").nullable();
t.foreign("bypasserUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.uuid("policyId").notNullable();
t.foreign("policyId").references("id").inTable(TableName.SecretApprovalPolicy).onDelete("CASCADE");
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.SecretApprovalPolicyBypasser);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.SecretApprovalPolicyBypasser);
await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyBypasser);
await dropOnUpdateTrigger(knex, TableName.SecretApprovalPolicyBypasser);
await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyBypasser);
}
@@ -0,0 +1,26 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const AccessApprovalPoliciesBypassersSchema = z.object({
id: z.string().uuid(),
bypasserGroupId: z.string().uuid().nullable().optional(),
bypasserUserId: z.string().uuid().nullable().optional(),
policyId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TAccessApprovalPoliciesBypassers = z.infer<typeof AccessApprovalPoliciesBypassersSchema>;
export type TAccessApprovalPoliciesBypassersInsert = Omit<
z.input<typeof AccessApprovalPoliciesBypassersSchema>,
TImmutableDBKeys
>;
export type TAccessApprovalPoliciesBypassersUpdate = Partial<
Omit<z.input<typeof AccessApprovalPoliciesBypassersSchema>, TImmutableDBKeys>
>;
+2
View File
@@ -1,5 +1,6 @@
export * from "./access-approval-policies"; export * from "./access-approval-policies";
export * from "./access-approval-policies-approvers"; export * from "./access-approval-policies-approvers";
export * from "./access-approval-policies-bypassers";
export * from "./access-approval-requests"; export * from "./access-approval-requests";
export * from "./access-approval-requests-reviewers"; export * from "./access-approval-requests-reviewers";
export * from "./api-keys"; export * from "./api-keys";
@@ -92,6 +93,7 @@ export * from "./saml-configs";
export * from "./scim-tokens"; export * from "./scim-tokens";
export * from "./secret-approval-policies"; export * from "./secret-approval-policies";
export * from "./secret-approval-policies-approvers"; export * from "./secret-approval-policies-approvers";
export * from "./secret-approval-policies-bypassers";
export * from "./secret-approval-request-secret-tags"; export * from "./secret-approval-request-secret-tags";
export * from "./secret-approval-request-secret-tags-v2"; export * from "./secret-approval-request-secret-tags-v2";
export * from "./secret-approval-requests"; export * from "./secret-approval-requests";
+2
View File
@@ -95,10 +95,12 @@ export enum TableName {
ScimToken = "scim_tokens", ScimToken = "scim_tokens",
AccessApprovalPolicy = "access_approval_policies", AccessApprovalPolicy = "access_approval_policies",
AccessApprovalPolicyApprover = "access_approval_policies_approvers", AccessApprovalPolicyApprover = "access_approval_policies_approvers",
AccessApprovalPolicyBypasser = "access_approval_policies_bypassers",
AccessApprovalRequest = "access_approval_requests", AccessApprovalRequest = "access_approval_requests",
AccessApprovalRequestReviewer = "access_approval_requests_reviewers", AccessApprovalRequestReviewer = "access_approval_requests_reviewers",
SecretApprovalPolicy = "secret_approval_policies", SecretApprovalPolicy = "secret_approval_policies",
SecretApprovalPolicyApprover = "secret_approval_policies_approvers", SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
SecretApprovalPolicyBypasser = "secret_approval_policies_bypassers",
SecretApprovalRequest = "secret_approval_requests", SecretApprovalRequest = "secret_approval_requests",
SecretApprovalRequestReviewer = "secret_approval_requests_reviewers", SecretApprovalRequestReviewer = "secret_approval_requests_reviewers",
SecretApprovalRequestSecret = "secret_approval_requests_secrets", SecretApprovalRequestSecret = "secret_approval_requests_secrets",
@@ -0,0 +1,26 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretApprovalPoliciesBypassersSchema = z.object({
id: z.string().uuid(),
bypasserGroupId: z.string().uuid().nullable().optional(),
bypasserUserId: z.string().uuid().nullable().optional(),
policyId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretApprovalPoliciesBypassers = z.infer<typeof SecretApprovalPoliciesBypassersSchema>;
export type TSecretApprovalPoliciesBypassersInsert = Omit<
z.input<typeof SecretApprovalPoliciesBypassersSchema>,
TImmutableDBKeys
>;
export type TSecretApprovalPoliciesBypassersUpdate = Partial<
Omit<z.input<typeof SecretApprovalPoliciesBypassersSchema>, TImmutableDBKeys>
>;
@@ -1,7 +1,7 @@
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types"; import { ApproverType, BypasserType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
import { EnforcementLevel } from "@app/lib/types"; import { EnforcementLevel } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -28,6 +28,13 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
]) ])
.array() .array()
.min(1, { message: "At least one approver should be provided" }), .min(1, { message: "At least one approver should be provided" }),
bypassers: z
.discriminatedUnion("type", [
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), name: z.string().optional() })
])
.array()
.optional(),
approvals: z.number().min(1).default(1), approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
allowedSelfApprovals: z.boolean().default(true) allowedSelfApprovals: z.boolean().default(true)
@@ -72,7 +79,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
.object({ type: z.nativeEnum(ApproverType), id: z.string().nullable().optional() }) .object({ type: z.nativeEnum(ApproverType), id: z.string().nullable().optional() })
.array() .array()
.nullable() .nullable()
.optional() .optional(),
bypassers: z.object({ type: z.nativeEnum(BypasserType), id: z.string().nullable().optional() }).array()
}) })
.array() .array()
.nullable() .nullable()
@@ -147,6 +155,13 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
]) ])
.array() .array()
.min(1, { message: "At least one approver should be provided" }), .min(1, { message: "At least one approver should be provided" }),
bypassers: z
.discriminatedUnion("type", [
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), name: z.string().optional() })
])
.array()
.optional(),
approvals: z.number().min(1).optional(), approvals: z.number().min(1).optional(),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
allowedSelfApprovals: z.boolean().default(true) allowedSelfApprovals: z.boolean().default(true)
@@ -220,6 +235,15 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
}) })
.array() .array()
.nullable() .nullable()
.optional(),
bypassers: z
.object({
type: z.nativeEnum(BypasserType),
id: z.string().nullable().optional(),
name: z.string().nullable().optional()
})
.array()
.nullable()
.optional() .optional()
}) })
}) })
@@ -113,6 +113,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
name: z.string(), name: z.string(),
approvals: z.number(), approvals: z.number(),
approvers: z.string().array(), approvers: z.string().array(),
bypassers: z.string().array(),
secretPath: z.string().nullish(), secretPath: z.string().nullish(),
envId: z.string(), envId: z.string(),
enforcementLevel: z.string(), enforcementLevel: z.string(),
@@ -1,7 +1,7 @@
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types"; import { ApproverType, BypasserType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { EnforcementLevel } from "@app/lib/types"; import { EnforcementLevel } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -34,6 +34,13 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
]) ])
.array() .array()
.min(1, { message: "At least one approver should be provided" }), .min(1, { message: "At least one approver should be provided" }),
bypassers: z
.discriminatedUnion("type", [
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), name: z.string().optional() })
])
.array()
.optional(),
approvals: z.number().min(1).default(1), approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
allowedSelfApprovals: z.boolean().default(true) allowedSelfApprovals: z.boolean().default(true)
@@ -79,6 +86,13 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
]) ])
.array() .array()
.min(1, { message: "At least one approver should be provided" }), .min(1, { message: "At least one approver should be provided" }),
bypassers: z
.discriminatedUnion("type", [
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), name: z.string().optional() })
])
.array()
.optional(),
approvals: z.number().min(1).default(1), approvals: z.number().min(1).default(1),
secretPath: z secretPath: z
.string() .string()
@@ -157,6 +171,12 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
id: z.string().nullable().optional(), id: z.string().nullable().optional(),
type: z.nativeEnum(ApproverType) type: z.nativeEnum(ApproverType)
}) })
.array(),
bypassers: z
.object({
id: z.string().nullable().optional(),
type: z.nativeEnum(BypasserType)
})
.array() .array()
}) })
.array() .array()
@@ -195,6 +215,13 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
type: z.nativeEnum(ApproverType), type: z.nativeEnum(ApproverType),
name: z.string().nullable().optional() name: z.string().nullable().optional()
}) })
.array(),
bypassers: z
.object({
id: z.string().nullable().optional(),
type: z.nativeEnum(BypasserType),
name: z.string().nullable().optional()
})
.array() .array()
}) })
}) })
@@ -47,6 +47,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
userId: z.string().nullable().optional() userId: z.string().nullable().optional()
}) })
.array(), .array(),
bypassers: z
.object({
userId: z.string().nullable().optional()
})
.array(),
secretPath: z.string().optional().nullable(), secretPath: z.string().optional().nullable(),
enforcementLevel: z.string(), enforcementLevel: z.string(),
deletedAt: z.date().nullish(), deletedAt: z.date().nullish(),
@@ -266,6 +271,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
name: z.string(), name: z.string(),
approvals: z.number(), approvals: z.number(),
approvers: approvalRequestUser.array(), approvers: approvalRequestUser.array(),
bypassers: approvalRequestUser.array(),
secretPath: z.string().optional().nullable(), secretPath: z.string().optional().nullable(),
enforcementLevel: z.string(), enforcementLevel: z.string(),
deletedAt: z.date().nullish(), deletedAt: z.date().nullish(),
@@ -8,3 +8,10 @@ export const accessApprovalPolicyApproverDALFactory = (db: TDbClient) => {
const accessApprovalPolicyApproverOrm = ormify(db, TableName.AccessApprovalPolicyApprover); const accessApprovalPolicyApproverOrm = ormify(db, TableName.AccessApprovalPolicyApprover);
return { ...accessApprovalPolicyApproverOrm }; return { ...accessApprovalPolicyApproverOrm };
}; };
export type TAccessApprovalPolicyBypasserDALFactory = ReturnType<typeof accessApprovalPolicyBypasserDALFactory>;
export const accessApprovalPolicyBypasserDALFactory = (db: TDbClient) => {
const accessApprovalPolicyBypasserOrm = ormify(db, TableName.AccessApprovalPolicyBypasser);
return { ...accessApprovalPolicyBypasserOrm };
};
@@ -1,11 +1,11 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { AccessApprovalPoliciesSchema, TableName, TAccessApprovalPolicies } from "@app/db/schemas"; import { AccessApprovalPoliciesSchema, TableName, TAccessApprovalPolicies, TUsers } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex"; import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
import { ApproverType } from "./access-approval-policy-types"; import { ApproverType, BypasserType } from "./access-approval-policy-types";
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>; export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
@@ -34,9 +34,22 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
`${TableName.AccessApprovalPolicyApprover}.policyId` `${TableName.AccessApprovalPolicyApprover}.policyId`
) )
.leftJoin(TableName.Users, `${TableName.AccessApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`) .leftJoin(TableName.Users, `${TableName.AccessApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
.leftJoin(
TableName.AccessApprovalPolicyBypasser,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyBypasser}.policyId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("bypasserUsers"),
`${TableName.AccessApprovalPolicyBypasser}.bypasserUserId`,
`bypasserUsers.id`
)
.select(tx.ref("username").withSchema(TableName.Users).as("approverUsername")) .select(tx.ref("username").withSchema(TableName.Users).as("approverUsername"))
.select(tx.ref("username").withSchema("bypasserUsers").as("bypasserUsername"))
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover)) .select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("approverGroupId").withSchema(TableName.AccessApprovalPolicyApprover)) .select(tx.ref("approverGroupId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser))
.select(tx.ref("bypasserGroupId").withSchema(TableName.AccessApprovalPolicyBypasser))
.select(tx.ref("name").withSchema(TableName.Environment).as("envName")) .select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug")) .select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(tx.ref("id").withSchema(TableName.Environment).as("envId")) .select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
@@ -129,6 +142,23 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
id, id,
type: ApproverType.Group type: ApproverType.Group
}) })
},
{
key: "bypasserUserId",
label: "bypassers" as const,
mapper: ({ bypasserUserId: id, bypasserUsername }) => ({
id,
type: BypasserType.User,
name: bypasserUsername
})
},
{
key: "bypasserGroupId",
label: "bypassers" as const,
mapper: ({ bypasserGroupId: id }) => ({
id,
type: BypasserType.Group
})
} }
] ]
}); });
@@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionApprovalActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
@@ -14,10 +14,14 @@ import { TAccessApprovalRequestReviewerDALFactory } from "../access-approval-req
import { ApprovalStatus } from "../access-approval-request/access-approval-request-types"; import { ApprovalStatus } from "../access-approval-request/access-approval-request-types";
import { TGroupDALFactory } from "../group/group-dal"; import { TGroupDALFactory } from "../group/group-dal";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal"; import {
TAccessApprovalPolicyApproverDALFactory,
TAccessApprovalPolicyBypasserDALFactory
} from "./access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal"; import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
import { import {
ApproverType, ApproverType,
BypasserType,
TCreateAccessApprovalPolicy, TCreateAccessApprovalPolicy,
TDeleteAccessApprovalPolicy, TDeleteAccessApprovalPolicy,
TGetAccessApprovalPolicyByIdDTO, TGetAccessApprovalPolicyByIdDTO,
@@ -32,6 +36,7 @@ type TAccessApprovalPolicyServiceFactoryDep = {
accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory; accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">;
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory; accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
accessApprovalPolicyBypasserDAL: TAccessApprovalPolicyBypasserDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
groupDAL: TGroupDALFactory; groupDAL: TGroupDALFactory;
userDAL: Pick<TUserDALFactory, "find">; userDAL: Pick<TUserDALFactory, "find">;
@@ -45,6 +50,7 @@ export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprov
export const accessApprovalPolicyServiceFactory = ({ export const accessApprovalPolicyServiceFactory = ({
accessApprovalPolicyDAL, accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
accessApprovalPolicyBypasserDAL,
groupDAL, groupDAL,
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
@@ -63,6 +69,7 @@ export const accessApprovalPolicyServiceFactory = ({
actorAuthMethod, actorAuthMethod,
approvals, approvals,
approvers, approvers,
bypassers,
projectSlug, projectSlug,
environment, environment,
enforcementLevel, enforcementLevel,
@@ -98,7 +105,7 @@ export const accessApprovalPolicyServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionApprovalActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
); );
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id }); const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
@@ -147,6 +154,44 @@ export const accessApprovalPolicyServiceFactory = ({
.map((user) => user.id); .map((user) => user.id);
verifyAllApprovers.push(...verifyGroupApprovers); verifyAllApprovers.push(...verifyGroupApprovers);
let groupBypassers: string[] = [];
let bypasserUserIds: string[] = [];
if (bypassers && bypassers.length) {
groupBypassers = bypassers
.filter((bypasser) => bypasser.type === BypasserType.Group)
.map((bypasser) => bypasser.id) as string[];
const userBypassers = bypassers
.filter((bypasser) => bypasser.type === BypasserType.User)
.map((bypasser) => bypasser.id)
.filter(Boolean) as string[];
const userBypasserNames = bypassers
.map((bypasser) => (bypasser.type === BypasserType.User ? bypasser.name : undefined))
.filter(Boolean) as string[];
bypasserUserIds = userBypassers;
if (userBypasserNames.length) {
const bypasserUsers = await userDAL.find({
$in: {
username: userBypasserNames
}
});
const bypasserNamesFromDb = bypasserUsers.map((user) => user.username);
const invalidUsernames = userBypasserNames.filter((username) => !bypasserNamesFromDb.includes(username));
if (invalidUsernames.length) {
throw new BadRequestError({
message: `Invalid bypasser user: ${invalidUsernames.join(", ")}`
});
}
bypasserUserIds = bypasserUserIds.concat(bypasserUsers.map((user) => user.id));
}
}
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => { const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.create( const doc = await accessApprovalPolicyDAL.create(
{ {
@@ -159,6 +204,7 @@ export const accessApprovalPolicyServiceFactory = ({
}, },
tx tx
); );
if (approverUserIds.length) { if (approverUserIds.length) {
await accessApprovalPolicyApproverDAL.insertMany( await accessApprovalPolicyApproverDAL.insertMany(
approverUserIds.map((userId) => ({ approverUserIds.map((userId) => ({
@@ -179,8 +225,29 @@ export const accessApprovalPolicyServiceFactory = ({
); );
} }
if (bypasserUserIds.length) {
await accessApprovalPolicyBypasserDAL.insertMany(
bypasserUserIds.map((userId) => ({
bypasserUserId: userId,
policyId: doc.id
})),
tx
);
}
if (groupBypassers.length) {
await accessApprovalPolicyBypasserDAL.insertMany(
groupBypassers.map((groupId) => ({
bypasserGroupId: groupId,
policyId: doc.id
})),
tx
);
}
return doc; return doc;
}); });
return { ...accessApproval, environment: env, projectId: project.id }; return { ...accessApproval, environment: env, projectId: project.id };
}; };
@@ -211,6 +278,7 @@ export const accessApprovalPolicyServiceFactory = ({
const updateAccessApprovalPolicy = async ({ const updateAccessApprovalPolicy = async ({
policyId, policyId,
approvers, approvers,
bypassers,
secretPath, secretPath,
name, name,
actorId, actorId,
@@ -256,10 +324,45 @@ export const accessApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
ProjectPermissionApprovalActions.Edit,
ProjectPermissionSub.SecretApproval let groupBypassers: string[] = [];
); let bypasserUserIds: string[] = [];
if (bypassers && bypassers.length) {
groupBypassers = bypassers
.filter((bypasser) => bypasser.type === BypasserType.Group)
.map((bypasser) => bypasser.id) as string[];
const userBypassers = bypassers
.filter((bypasser) => bypasser.type === BypasserType.User)
.map((bypasser) => bypasser.id)
.filter(Boolean) as string[];
const userBypasserNames = bypassers
.map((bypasser) => (bypasser.type === BypasserType.User ? bypasser.name : undefined))
.filter(Boolean) as string[];
bypasserUserIds = userBypassers;
if (userBypasserNames.length) {
const bypasserUsers = await userDAL.find({
$in: {
username: userBypasserNames
}
});
const bypasserNamesFromDb = bypasserUsers.map((user) => user.username);
const invalidUsernames = userBypasserNames.filter((username) => !bypasserNamesFromDb.includes(username));
if (invalidUsernames.length) {
throw new BadRequestError({
message: `Invalid bypasser user: ${invalidUsernames.join(", ")}`
});
}
bypasserUserIds = bypasserUserIds.concat(bypasserUsers.map((user) => user.id));
}
}
const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.updateById( const doc = await accessApprovalPolicyDAL.updateById(
@@ -316,6 +419,28 @@ export const accessApprovalPolicyServiceFactory = ({
); );
} }
await accessApprovalPolicyBypasserDAL.delete({ policyId: doc.id }, tx);
if (bypasserUserIds.length) {
await accessApprovalPolicyBypasserDAL.insertMany(
bypasserUserIds.map((userId) => ({
bypasserUserId: userId,
policyId: doc.id
})),
tx
);
}
if (groupBypassers.length) {
await accessApprovalPolicyBypasserDAL.insertMany(
groupBypassers.map((groupId) => ({
bypasserGroupId: groupId,
policyId: doc.id
})),
tx
);
}
return doc; return doc;
}); });
return { return {
@@ -344,7 +469,7 @@ export const accessApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionApprovalActions.Delete, ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
); );
@@ -435,10 +560,7 @@ export const accessApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
ProjectPermissionApprovalActions.Read,
ProjectPermissionSub.SecretApproval
);
return policy; return policy;
}; };
@@ -18,11 +18,17 @@ export enum ApproverType {
User = "user" User = "user"
} }
export enum BypasserType {
Group = "group",
User = "user"
}
export type TCreateAccessApprovalPolicy = { export type TCreateAccessApprovalPolicy = {
approvals: number; approvals: number;
secretPath: string; secretPath: string;
environment: string; environment: string;
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[]; approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
bypassers?: ({ type: BypasserType.Group; id: string } | { type: BypasserType.User; id?: string; name?: string })[];
projectSlug: string; projectSlug: string;
name: string; name: string;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
@@ -33,6 +39,7 @@ export type TUpdateAccessApprovalPolicy = {
policyId: string; policyId: string;
approvals?: number; approvals?: number;
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[]; approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
bypassers?: ({ type: BypasserType.Group; id: string } | { type: BypasserType.User; id?: string; name?: string })[];
secretPath?: string; secretPath?: string;
name?: string; name?: string;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
@@ -1,7 +1,13 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { AccessApprovalRequestsSchema, TableName, TAccessApprovalRequests, TUsers } from "@app/db/schemas"; import {
AccessApprovalRequestsSchema,
TableName,
TAccessApprovalRequests,
TUserGroupMembership,
TUsers
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
@@ -28,12 +34,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.AccessApprovalRequest}.policyId`, `${TableName.AccessApprovalRequest}.policyId`,
`${TableName.AccessApprovalPolicy}.id` `${TableName.AccessApprovalPolicy}.id`
) )
.leftJoin( .leftJoin(
TableName.AccessApprovalRequestReviewer, TableName.AccessApprovalRequestReviewer,
`${TableName.AccessApprovalRequest}.id`, `${TableName.AccessApprovalRequest}.id`,
`${TableName.AccessApprovalRequestReviewer}.requestId` `${TableName.AccessApprovalRequestReviewer}.requestId`
) )
.leftJoin( .leftJoin(
TableName.AccessApprovalPolicyApprover, TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`, `${TableName.AccessApprovalPolicy}.id`,
@@ -46,6 +52,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
) )
.leftJoin(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) .leftJoin(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(
TableName.AccessApprovalPolicyBypasser,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyBypasser}.policyId`
)
.leftJoin<TUserGroupMembership>(
db(TableName.UserGroupMembership).as("bypasserUserGroupMembership"),
`${TableName.AccessApprovalPolicyBypasser}.bypasserGroupId`,
`bypasserUserGroupMembership.groupId`
)
.join<TUsers>( .join<TUsers>(
db(TableName.Users).as("requestedByUser"), db(TableName.Users).as("requestedByUser"),
`${TableName.AccessApprovalRequest}.requestedByUserId`, `${TableName.AccessApprovalRequest}.requestedByUserId`,
@@ -69,6 +86,9 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover)) .select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId")) .select(db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"))
.select(db.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser))
.select(db.ref("userId").withSchema("bypasserUserGroupMembership").as("bypasserGroupUserId"))
.select( .select(
db.ref("projectId").withSchema(TableName.Environment), db.ref("projectId").withSchema(TableName.Environment),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"), db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
@@ -158,6 +178,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
key: "approverGroupUserId", key: "approverGroupUserId",
label: "approvers" as const, label: "approvers" as const,
mapper: ({ approverGroupUserId }) => approverGroupUserId mapper: ({ approverGroupUserId }) => approverGroupUserId
},
{ key: "bypasserUserId", label: "bypassers" as const, mapper: ({ bypasserUserId }) => bypasserUserId },
{
key: "bypasserGroupUserId",
label: "bypassers" as const,
mapper: ({ bypasserGroupUserId }) => bypasserGroupUserId
} }
] ]
}); });
@@ -166,7 +192,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
return formattedDocs.map((doc) => ({ return formattedDocs.map((doc) => ({
...doc, ...doc,
policy: { ...doc.policy, approvers: doc.approvers } policy: { ...doc.policy, approvers: doc.approvers, bypassers: doc.bypassers }
})); }));
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "FindRequestsWithPrivilege" }); throw new DatabaseError({ error, name: "FindRequestsWithPrivilege" });
@@ -193,7 +219,6 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.AccessApprovalPolicy}.id`, `${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId` `${TableName.AccessApprovalPolicyApprover}.policyId`
) )
.leftJoin<TUsers>( .leftJoin<TUsers>(
db(TableName.Users).as("accessApprovalPolicyApproverUser"), db(TableName.Users).as("accessApprovalPolicyApproverUser"),
`${TableName.AccessApprovalPolicyApprover}.approverUserId`, `${TableName.AccessApprovalPolicyApprover}.approverUserId`,
@@ -204,13 +229,33 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`, `${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId` `${TableName.UserGroupMembership}.groupId`
) )
.leftJoin<TUsers>( .leftJoin<TUsers>(
db(TableName.Users).as("accessApprovalPolicyGroupApproverUser"), db(TableName.Users).as("accessApprovalPolicyGroupApproverUser"),
`${TableName.UserGroupMembership}.userId`, `${TableName.UserGroupMembership}.userId`,
"accessApprovalPolicyGroupApproverUser.id" "accessApprovalPolicyGroupApproverUser.id"
) )
.leftJoin(
TableName.AccessApprovalPolicyBypasser,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyBypasser}.policyId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("accessApprovalPolicyBypasserUser"),
`${TableName.AccessApprovalPolicyBypasser}.bypasserUserId`,
"accessApprovalPolicyBypasserUser.id"
)
.leftJoin<TUserGroupMembership>(
db(TableName.UserGroupMembership).as("bypasserUserGroupMembership"),
`${TableName.AccessApprovalPolicyBypasser}.bypasserGroupId`,
`bypasserUserGroupMembership.groupId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("accessApprovalPolicyGroupBypasserUser"),
`bypasserUserGroupMembership.userId`,
"accessApprovalPolicyGroupBypasserUser.id"
)
.leftJoin( .leftJoin(
TableName.AccessApprovalRequestReviewer, TableName.AccessApprovalRequestReviewer,
`${TableName.AccessApprovalRequest}.id`, `${TableName.AccessApprovalRequest}.id`,
@@ -241,6 +286,18 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"), tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
tx.ref("lastName").withSchema("requestedByUser").as("requestedByUserLastName"), tx.ref("lastName").withSchema("requestedByUser").as("requestedByUserLastName"),
// Bypassers
tx.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser),
tx.ref("userId").withSchema("bypasserUserGroupMembership").as("bypasserGroupUserId"),
tx.ref("email").withSchema("accessApprovalPolicyBypasserUser").as("bypasserEmail"),
tx.ref("email").withSchema("accessApprovalPolicyGroupBypasserUser").as("bypasserGroupEmail"),
tx.ref("username").withSchema("accessApprovalPolicyBypasserUser").as("bypasserUsername"),
tx.ref("username").withSchema("accessApprovalPolicyGroupBypasserUser").as("bypasserGroupUsername"),
tx.ref("firstName").withSchema("accessApprovalPolicyBypasserUser").as("bypasserFirstName"),
tx.ref("firstName").withSchema("accessApprovalPolicyGroupBypasserUser").as("bypasserGroupFirstName"),
tx.ref("lastName").withSchema("accessApprovalPolicyBypasserUser").as("bypasserLastName"),
tx.ref("lastName").withSchema("accessApprovalPolicyGroupBypasserUser").as("bypasserGroupLastName"),
tx.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer), tx.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer),
tx.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"), tx.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"),
@@ -265,7 +322,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
try { try {
const sql = findQuery({ [`${TableName.AccessApprovalRequest}.id` as "id"]: id }, tx || db.replicaNode()); const sql = findQuery({ [`${TableName.AccessApprovalRequest}.id` as "id"]: id }, tx || db.replicaNode());
const docs = await sql; const docs = await sql;
const formatedDoc = sqlNestRelationships({ const formattedDoc = sqlNestRelationships({
data: docs, data: docs,
key: "id", key: "id",
parentMapper: (el) => ({ parentMapper: (el) => ({
@@ -335,13 +392,51 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
lastName, lastName,
username username
}) })
},
{
key: "bypasserUserId",
label: "bypassers" as const,
mapper: ({
bypasserUserId,
bypasserEmail: email,
bypasserUsername: username,
bypasserLastName: lastName,
bypasserFirstName: firstName
}) => ({
userId: bypasserUserId,
email,
firstName,
lastName,
username
})
},
{
key: "bypasserGroupUserId",
label: "bypassers" as const,
mapper: ({
userId,
bypasserGroupEmail: email,
bypasserGroupUsername: username,
bypasserGroupLastName: lastName,
bypasserFirstName: firstName
}) => ({
userId,
email,
firstName,
lastName,
username
})
} }
] ]
}); });
if (!formatedDoc?.[0]) return; if (!formattedDoc?.[0]) return;
return { return {
...formatedDoc[0], ...formattedDoc[0],
policy: { ...formatedDoc[0].policy, approvers: formatedDoc[0].approvers } policy: {
...formattedDoc[0].policy,
approvers: formattedDoc[0].approvers,
bypassers: formattedDoc[0].bypassers
}
}; };
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "FindByIdAccessApprovalRequest" }); throw new DatabaseError({ error, name: "FindByIdAccessApprovalRequest" });
@@ -23,7 +23,6 @@ import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-poli
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
import { TGroupDALFactory } from "../group/group-dal"; import { TGroupDALFactory } from "../group/group-dal";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionApprovalActions, ProjectPermissionSub } from "../permission/project-permission";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal"; import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal";
@@ -340,7 +339,7 @@ export const accessApprovalRequestServiceFactory = ({
}); });
} }
const { membership, hasRole, permission } = await permissionService.getProjectPermission({ const { membership, hasRole } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId: accessApprovalRequest.projectId, projectId: accessApprovalRequest.projectId,
@@ -355,13 +354,12 @@ export const accessApprovalRequestServiceFactory = ({
const isSelfApproval = actorId === accessApprovalRequest.requestedByUserId; const isSelfApproval = actorId === accessApprovalRequest.requestedByUserId;
const isSoftEnforcement = policy.enforcementLevel === EnforcementLevel.Soft; const isSoftEnforcement = policy.enforcementLevel === EnforcementLevel.Soft;
const canBypassApproval = permission.can( const canBypass = !policy.bypassers.length || policy.bypassers.some((bypasser) => bypasser.userId === actorId);
ProjectPermissionApprovalActions.AllowAccessBypass, const cannotBypassUnderSoftEnforcement = !(isSoftEnforcement && canBypass);
ProjectPermissionSub.SecretApproval
);
const cannotBypassUnderSoftEnforcement = !(isSoftEnforcement && canBypassApproval);
if (!policy.allowedSelfApprovals && isSelfApproval && cannotBypassUnderSoftEnforcement) { const isApprover = policy.approvers.find((approver) => approver.userId === actorId);
if ((!isApprover || (!policy.allowedSelfApprovals && isSelfApproval)) && cannotBypassUnderSoftEnforcement) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to review access approval request. Users are not authorized to review their own request." message: "Failed to review access approval request. Users are not authorized to review their own request."
}); });
@@ -370,7 +368,7 @@ export const accessApprovalRequestServiceFactory = ({
if ( if (
!hasRole(ProjectMembershipRole.Admin) && !hasRole(ProjectMembershipRole.Admin) &&
accessApprovalRequest.requestedByUserId !== actorId && // The request wasn't made by the current user accessApprovalRequest.requestedByUserId !== actorId && // The request wasn't made by the current user
!policy.approvers.find((approver) => approver.userId === actorId) // The request isn't performed by an assigned approver !isApprover // The request isn't performed by an assigned approver
) { ) {
throw new ForbiddenRequestError({ message: "You are not authorized to approve this request" }); throw new ForbiddenRequestError({ message: "You are not authorized to approve this request" });
} }
@@ -2,7 +2,6 @@ import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability"
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionApprovalActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCmekActions, ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
@@ -57,12 +56,10 @@ const buildAdminPermissionRules = () => {
can( can(
[ [
ProjectPermissionApprovalActions.Read, ProjectPermissionActions.Read,
ProjectPermissionApprovalActions.Edit, ProjectPermissionActions.Edit,
ProjectPermissionApprovalActions.Create, ProjectPermissionActions.Create,
ProjectPermissionApprovalActions.Delete, ProjectPermissionActions.Delete
ProjectPermissionApprovalActions.AllowChangeBypass,
ProjectPermissionApprovalActions.AllowAccessBypass
], ],
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
); );
@@ -255,7 +252,7 @@ const buildMemberPermissionRules = () => {
ProjectPermissionSub.SecretImports ProjectPermissionSub.SecretImports
); );
can([ProjectPermissionApprovalActions.Read], ProjectPermissionSub.SecretApproval); can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation); can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation);
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback); can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
@@ -403,7 +400,7 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
can(ProjectPermissionApprovalActions.Read, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation); can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
@@ -34,15 +34,6 @@ export enum ProjectPermissionSecretActions {
Delete = "delete" Delete = "delete"
} }
export enum ProjectPermissionApprovalActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
AllowChangeBypass = "allow-change-bypass",
AllowAccessBypass = "allow-access-bypass"
}
export enum ProjectPermissionCmekActions { export enum ProjectPermissionCmekActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -251,7 +242,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionApprovalActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ | [
ProjectPermissionSecretRotationActions, ProjectPermissionSecretRotationActions,
( (
@@ -448,7 +439,7 @@ const PkiSubscriberConditionSchema = z
const GeneralPermissionSchema = [ const GeneralPermissionSchema = [
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionApprovalActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}), }),
@@ -8,3 +8,10 @@ export const secretApprovalPolicyApproverDALFactory = (db: TDbClient) => {
const sapApproverOrm = ormify(db, TableName.SecretApprovalPolicyApprover); const sapApproverOrm = ormify(db, TableName.SecretApprovalPolicyApprover);
return sapApproverOrm; return sapApproverOrm;
}; };
export type TSecretApprovalPolicyBypasserDALFactory = ReturnType<typeof secretApprovalPolicyBypasserDALFactory>;
export const secretApprovalPolicyBypasserDALFactory = (db: TDbClient) => {
const sapBypasserOrm = ormify(db, TableName.SecretApprovalPolicyBypasser);
return sapBypasserOrm;
};
@@ -1,11 +1,17 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { SecretApprovalPoliciesSchema, TableName, TSecretApprovalPolicies, TUsers } from "@app/db/schemas"; import {
SecretApprovalPoliciesSchema,
TableName,
TSecretApprovalPolicies,
TUserGroupMembership,
TUsers
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex"; import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
import { ApproverType } from "../access-approval-policy/access-approval-policy-types"; import { ApproverType, BypasserType } from "../access-approval-policy/access-approval-policy-types";
export type TSecretApprovalPolicyDALFactory = ReturnType<typeof secretApprovalPolicyDALFactory>; export type TSecretApprovalPolicyDALFactory = ReturnType<typeof secretApprovalPolicyDALFactory>;
@@ -43,6 +49,22 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalPolicyApprover}.approverUserId`, `${TableName.SecretApprovalPolicyApprover}.approverUserId`,
"secretApprovalPolicyApproverUser.id" "secretApprovalPolicyApproverUser.id"
) )
// Bypasser
.leftJoin(
TableName.SecretApprovalPolicyBypasser,
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyBypasser}.policyId`
)
.leftJoin<TUserGroupMembership>(
db(TableName.UserGroupMembership).as("bypasserUserGroupMembership"),
`${TableName.SecretApprovalPolicyBypasser}.bypasserGroupId`,
`bypasserUserGroupMembership.groupId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("secretApprovalPolicyBypasserUser"),
`${TableName.SecretApprovalPolicyBypasser}.bypasserUserId`,
"secretApprovalPolicyBypasserUser.id"
)
.leftJoin<TUsers>(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) .leftJoin<TUsers>(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.select( .select(
tx.ref("id").withSchema("secretApprovalPolicyApproverUser").as("approverUserId"), tx.ref("id").withSchema("secretApprovalPolicyApproverUser").as("approverUserId"),
@@ -58,6 +80,20 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
tx.ref("firstName").withSchema(TableName.Users).as("approverGroupFirstName"), tx.ref("firstName").withSchema(TableName.Users).as("approverGroupFirstName"),
tx.ref("lastName").withSchema(TableName.Users).as("approverGroupLastName") tx.ref("lastName").withSchema(TableName.Users).as("approverGroupLastName")
) )
.select(
tx.ref("id").withSchema("secretApprovalPolicyBypasserUser").as("bypasserUserId"),
tx.ref("email").withSchema("secretApprovalPolicyBypasserUser").as("bypasserEmail"),
tx.ref("firstName").withSchema("secretApprovalPolicyBypasserUser").as("bypasserFirstName"),
tx.ref("username").withSchema("secretApprovalPolicyBypasserUser").as("bypasserUsername"),
tx.ref("lastName").withSchema("secretApprovalPolicyBypasserUser").as("bypasserLastName")
)
.select(
tx.ref("bypasserGroupId").withSchema(TableName.SecretApprovalPolicyBypasser),
tx.ref("userId").withSchema("bypasserUserGroupMembership").as("bypasserGroupUserId"),
tx.ref("email").withSchema(TableName.Users).as("bypasserGroupEmail"),
tx.ref("firstName").withSchema(TableName.Users).as("bypasserGroupFirstName"),
tx.ref("lastName").withSchema(TableName.Users).as("bypasserGroupLastName")
)
.select( .select(
tx.ref("name").withSchema(TableName.Environment).as("envName"), tx.ref("name").withSchema(TableName.Environment).as("envName"),
tx.ref("slug").withSchema(TableName.Environment).as("envSlug"), tx.ref("slug").withSchema(TableName.Environment).as("envSlug"),
@@ -155,6 +191,23 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
id id
}) })
}, },
{
key: "bypasserUserId",
label: "bypassers" as const,
mapper: ({ bypasserUserId: id, bypasserUsername }) => ({
type: BypasserType.User,
name: bypasserUsername,
id
})
},
{
key: "bypasserGroupId",
label: "bypassers" as const,
mapper: ({ bypasserGroupId: id }) => ({
type: BypasserType.Group,
id
})
},
{ {
key: "approverUserId", key: "approverUserId",
label: "userApprovers" as const, label: "userApprovers" as const,
@@ -3,18 +3,21 @@ import picomatch from "picomatch";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionApprovalActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { containsGlobPatterns } from "@app/lib/picomatch"; import { containsGlobPatterns } from "@app/lib/picomatch";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { ApproverType } from "../access-approval-policy/access-approval-policy-types"; import { ApproverType, BypasserType } from "../access-approval-policy/access-approval-policy-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { TSecretApprovalRequestDALFactory } from "../secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestDALFactory } from "../secret-approval-request/secret-approval-request-dal";
import { RequestState } from "../secret-approval-request/secret-approval-request-types"; import { RequestState } from "../secret-approval-request/secret-approval-request-types";
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal"; import {
TSecretApprovalPolicyApproverDALFactory,
TSecretApprovalPolicyBypasserDALFactory
} from "./secret-approval-policy-approver-dal";
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal"; import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
import { import {
TCreateSapDTO, TCreateSapDTO,
@@ -36,6 +39,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
userDAL: Pick<TUserDALFactory, "find">; userDAL: Pick<TUserDALFactory, "find">;
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory; secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
secretApprovalPolicyBypasserDAL: TSecretApprovalPolicyBypasserDALFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "update">; secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "update">;
}; };
@@ -46,6 +50,7 @@ export const secretApprovalPolicyServiceFactory = ({
secretApprovalPolicyDAL, secretApprovalPolicyDAL,
permissionService, permissionService,
secretApprovalPolicyApproverDAL, secretApprovalPolicyApproverDAL,
secretApprovalPolicyBypasserDAL,
projectEnvDAL, projectEnvDAL,
userDAL, userDAL,
licenseService, licenseService,
@@ -59,6 +64,7 @@ export const secretApprovalPolicyServiceFactory = ({
actorAuthMethod, actorAuthMethod,
approvals, approvals,
approvers, approvers,
bypassers,
projectId, projectId,
secretPath, secretPath,
environment, environment,
@@ -89,7 +95,7 @@ export const secretApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionApprovalActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
); );
@@ -107,6 +113,44 @@ export const secretApprovalPolicyServiceFactory = ({
message: `Environment with slug '${environment}' not found in project with ID ${projectId}` message: `Environment with slug '${environment}' not found in project with ID ${projectId}`
}); });
let groupBypassers: string[] = [];
let bypasserUserIds: string[] = [];
if (bypassers && bypassers.length) {
groupBypassers = bypassers
.filter((bypasser) => bypasser.type === BypasserType.Group)
.map((bypasser) => bypasser.id) as string[];
const userBypassers = bypassers
.filter((bypasser) => bypasser.type === BypasserType.User)
.map((bypasser) => bypasser.id)
.filter(Boolean) as string[];
const userBypasserNames = bypassers
.map((bypasser) => (bypasser.type === BypasserType.User ? bypasser.name : undefined))
.filter(Boolean) as string[];
bypasserUserIds = userBypassers;
if (userBypasserNames.length) {
const bypasserUsers = await userDAL.find({
$in: {
username: userBypasserNames
}
});
const bypasserNamesFromDb = bypasserUsers.map((user) => user.username);
const invalidUsernames = userBypasserNames.filter((username) => !bypasserNamesFromDb.includes(username));
if (invalidUsernames.length) {
throw new BadRequestError({
message: `Invalid bypasser user: ${invalidUsernames.join(", ")}`
});
}
bypasserUserIds = bypasserUserIds.concat(bypasserUsers.map((user) => user.id));
}
}
const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => { const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => {
const doc = await secretApprovalPolicyDAL.create( const doc = await secretApprovalPolicyDAL.create(
{ {
@@ -158,6 +202,27 @@ export const secretApprovalPolicyServiceFactory = ({
})), })),
tx tx
); );
if (bypasserUserIds.length) {
await secretApprovalPolicyBypasserDAL.insertMany(
bypasserUserIds.map((userId) => ({
bypasserUserId: userId,
policyId: doc.id
})),
tx
);
}
if (groupBypassers.length) {
await secretApprovalPolicyBypasserDAL.insertMany(
groupBypassers.map((groupId) => ({
bypasserGroupId: groupId,
policyId: doc.id
})),
tx
);
}
return doc; return doc;
}); });
@@ -166,6 +231,7 @@ export const secretApprovalPolicyServiceFactory = ({
const updateSecretApprovalPolicy = async ({ const updateSecretApprovalPolicy = async ({
approvers, approvers,
bypassers,
secretPath, secretPath,
name, name,
actorId, actorId,
@@ -204,10 +270,7 @@ export const secretApprovalPolicyServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
ProjectPermissionApprovalActions.Edit,
ProjectPermissionSub.SecretApproval
);
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) { if (!plan.secretApproval) {
@@ -217,6 +280,44 @@ export const secretApprovalPolicyServiceFactory = ({
}); });
} }
let groupBypassers: string[] = [];
let bypasserUserIds: string[] = [];
if (bypassers && bypassers.length) {
groupBypassers = bypassers
.filter((bypasser) => bypasser.type === BypasserType.Group)
.map((bypasser) => bypasser.id) as string[];
const userBypassers = bypassers
.filter((bypasser) => bypasser.type === BypasserType.User)
.map((bypasser) => bypasser.id)
.filter(Boolean) as string[];
const userBypasserNames = bypassers
.map((bypasser) => (bypasser.type === BypasserType.User ? bypasser.name : undefined))
.filter(Boolean) as string[];
bypasserUserIds = userBypassers;
if (userBypasserNames.length) {
const bypasserUsers = await userDAL.find({
$in: {
username: userBypasserNames
}
});
const bypasserNamesFromDb = bypasserUsers.map((user) => user.username);
const invalidUsernames = userBypasserNames.filter((username) => !bypasserNamesFromDb.includes(username));
if (invalidUsernames.length) {
throw new BadRequestError({
message: `Invalid bypasser user: ${invalidUsernames.join(", ")}`
});
}
bypasserUserIds = bypasserUserIds.concat(bypasserUsers.map((user) => user.id));
}
}
const updatedSap = await secretApprovalPolicyDAL.transaction(async (tx) => { const updatedSap = await secretApprovalPolicyDAL.transaction(async (tx) => {
const doc = await secretApprovalPolicyDAL.updateById( const doc = await secretApprovalPolicyDAL.updateById(
secretApprovalPolicy.id, secretApprovalPolicy.id,
@@ -275,6 +376,28 @@ export const secretApprovalPolicyServiceFactory = ({
); );
} }
await secretApprovalPolicyBypasserDAL.delete({ policyId: doc.id }, tx);
if (bypasserUserIds.length) {
await secretApprovalPolicyBypasserDAL.insertMany(
bypasserUserIds.map((userId) => ({
bypasserUserId: userId,
policyId: doc.id
})),
tx
);
}
if (groupBypassers.length) {
await secretApprovalPolicyBypasserDAL.insertMany(
groupBypassers.map((groupId) => ({
bypasserGroupId: groupId,
policyId: doc.id
})),
tx
);
}
return doc; return doc;
}); });
return { return {
@@ -304,7 +427,7 @@ export const secretApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionApprovalActions.Delete, ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
); );
@@ -343,10 +466,7 @@ export const secretApprovalPolicyServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
ProjectPermissionApprovalActions.Read,
ProjectPermissionSub.SecretApproval
);
const sapPolicies = await secretApprovalPolicyDAL.find({ projectId, deletedAt: null }); const sapPolicies = await secretApprovalPolicyDAL.find({ projectId, deletedAt: null });
return sapPolicies; return sapPolicies;
@@ -419,10 +539,7 @@ export const secretApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
ProjectPermissionApprovalActions.Read,
ProjectPermissionSub.SecretApproval
);
return sapPolicy; return sapPolicy;
}; };
@@ -1,12 +1,13 @@
import { EnforcementLevel, TProjectPermission } from "@app/lib/types"; import { EnforcementLevel, TProjectPermission } from "@app/lib/types";
import { ApproverType } from "../access-approval-policy/access-approval-policy-types"; import { ApproverType, BypasserType } from "../access-approval-policy/access-approval-policy-types";
export type TCreateSapDTO = { export type TCreateSapDTO = {
approvals: number; approvals: number;
secretPath?: string | null; secretPath?: string | null;
environment: string; environment: string;
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[]; approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
bypassers?: ({ type: BypasserType.Group; id: string } | { type: BypasserType.User; id?: string; name?: string })[];
projectId: string; projectId: string;
name: string; name: string;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
@@ -18,6 +19,7 @@ export type TUpdateSapDTO = {
approvals?: number; approvals?: number;
secretPath?: string | null; secretPath?: string | null;
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[]; approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
bypassers?: ({ type: BypasserType.Group; id: string } | { type: BypasserType.User; id?: string; name?: string })[];
name?: string; name?: string;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
allowedSelfApprovals?: boolean; allowedSelfApprovals?: boolean;
@@ -6,6 +6,7 @@ import {
TableName, TableName,
TSecretApprovalRequests, TSecretApprovalRequests,
TSecretApprovalRequestsSecrets, TSecretApprovalRequestsSecrets,
TUserGroupMembership,
TUsers TUsers
} from "@app/db/schemas"; } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
@@ -58,16 +59,36 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalPolicyApprover}.approverUserId`, `${TableName.SecretApprovalPolicyApprover}.approverUserId`,
"secretApprovalPolicyApproverUser.id" "secretApprovalPolicyApproverUser.id"
) )
.leftJoin( .leftJoin<TUserGroupMembership>(
TableName.UserGroupMembership, db(TableName.UserGroupMembership).as("approverUserGroupMembership"),
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`, `${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId` `approverUserGroupMembership.groupId`
) )
.leftJoin<TUsers>( .leftJoin<TUsers>(
db(TableName.Users).as("secretApprovalPolicyGroupApproverUser"), db(TableName.Users).as("secretApprovalPolicyGroupApproverUser"),
`${TableName.UserGroupMembership}.userId`, `approverUserGroupMembership.userId`,
`secretApprovalPolicyGroupApproverUser.id` `secretApprovalPolicyGroupApproverUser.id`
) )
.leftJoin(
TableName.SecretApprovalPolicyBypasser,
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyBypasser}.policyId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("secretApprovalPolicyBypasserUser"),
`${TableName.SecretApprovalPolicyBypasser}.bypasserUserId`,
"secretApprovalPolicyBypasserUser.id"
)
.leftJoin<TUserGroupMembership>(
db(TableName.UserGroupMembership).as("bypasserUserGroupMembership"),
`${TableName.SecretApprovalPolicyBypasser}.bypasserGroupId`,
`bypasserUserGroupMembership.groupId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("secretApprovalPolicyGroupBypasserUser"),
`bypasserUserGroupMembership.userId`,
`secretApprovalPolicyGroupBypasserUser.id`
)
.leftJoin( .leftJoin(
TableName.SecretApprovalRequestReviewer, TableName.SecretApprovalRequestReviewer,
`${TableName.SecretApprovalRequest}.id`, `${TableName.SecretApprovalRequest}.id`,
@@ -81,7 +102,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.SecretApprovalRequest)) .select(selectAllTableCols(TableName.SecretApprovalRequest))
.select( .select(
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
tx.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"), tx.ref("userId").withSchema("approverUserGroupMembership").as("approverGroupUserId"),
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"), tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
tx.ref("email").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupEmail"), tx.ref("email").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"), tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
@@ -90,6 +111,20 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
tx.ref("firstName").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupFirstName"), tx.ref("firstName").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupFirstName"),
tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName"), tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName"),
tx.ref("lastName").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupLastName"), tx.ref("lastName").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupLastName"),
// Bypasser fields
tx.ref("bypasserUserId").withSchema(TableName.SecretApprovalPolicyBypasser),
tx.ref("bypasserGroupId").withSchema(TableName.SecretApprovalPolicyBypasser),
tx.ref("userId").withSchema("bypasserUserGroupMembership").as("bypasserGroupUserId"),
tx.ref("email").withSchema("secretApprovalPolicyBypasserUser").as("bypasserEmail"),
tx.ref("email").withSchema("secretApprovalPolicyGroupBypasserUser").as("bypasserGroupEmail"),
tx.ref("username").withSchema("secretApprovalPolicyBypasserUser").as("bypasserUsername"),
tx.ref("username").withSchema("secretApprovalPolicyGroupBypasserUser").as("bypasserGroupUsername"),
tx.ref("firstName").withSchema("secretApprovalPolicyBypasserUser").as("bypasserFirstName"),
tx.ref("firstName").withSchema("secretApprovalPolicyGroupBypasserUser").as("bypasserGroupFirstName"),
tx.ref("lastName").withSchema("secretApprovalPolicyBypasserUser").as("bypasserLastName"),
tx.ref("lastName").withSchema("secretApprovalPolicyGroupBypasserUser").as("bypasserGroupLastName"),
tx.ref("email").withSchema("statusChangedByUser").as("statusChangedByUserEmail"), tx.ref("email").withSchema("statusChangedByUser").as("statusChangedByUserEmail"),
tx.ref("username").withSchema("statusChangedByUser").as("statusChangedByUserUsername"), tx.ref("username").withSchema("statusChangedByUser").as("statusChangedByUserUsername"),
tx.ref("firstName").withSchema("statusChangedByUser").as("statusChangedByUserFirstName"), tx.ref("firstName").withSchema("statusChangedByUser").as("statusChangedByUserFirstName"),
@@ -121,7 +156,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
try { try {
const sql = findQuery({ [`${TableName.SecretApprovalRequest}.id` as "id"]: id }, tx || db.replicaNode()); const sql = findQuery({ [`${TableName.SecretApprovalRequest}.id` as "id"]: id }, tx || db.replicaNode());
const docs = await sql; const docs = await sql;
const formatedDoc = sqlNestRelationships({ const formattedDoc = sqlNestRelationships({
data: docs, data: docs,
key: "id", key: "id",
parentMapper: (el) => ({ parentMapper: (el) => ({
@@ -203,13 +238,51 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
lastName, lastName,
username username
}) })
},
{
key: "bypasserUserId",
label: "bypassers" as const,
mapper: ({
bypasserUserId: userId,
bypasserEmail: email,
bypasserUsername: username,
bypasserLastName: lastName,
bypasserFirstName: firstName
}) => ({
userId,
email,
firstName,
lastName,
username
})
},
{
key: "bypasserGroupUserId",
label: "bypassers" as const,
mapper: ({
bypasserGroupUserId: userId,
bypasserGroupEmail: email,
bypasserGroupUsername: username,
bypasserGroupLastName: lastName,
bypasserGroupFirstName: firstName
}) => ({
userId,
email,
firstName,
lastName,
username
})
} }
] ]
}); });
if (!formatedDoc?.[0]) return; if (!formattedDoc?.[0]) return;
return { return {
...formatedDoc[0], ...formattedDoc[0],
policy: { ...formatedDoc[0].policy, approvers: formatedDoc[0].approvers } policy: {
...formattedDoc[0].policy,
approvers: formattedDoc[0].approvers,
bypassers: formattedDoc[0].bypassers
}
}; };
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "FindByIdSAR" }); throw new DatabaseError({ error, name: "FindByIdSAR" });
@@ -291,6 +364,16 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`, `${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId` `${TableName.UserGroupMembership}.groupId`
) )
.leftJoin(
TableName.SecretApprovalPolicyBypasser,
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyBypasser}.policyId`
)
.leftJoin<TUserGroupMembership>(
db(TableName.UserGroupMembership).as("bypasserUserGroupMembership"),
`${TableName.SecretApprovalPolicyBypasser}.bypasserGroupId`,
`bypasserUserGroupMembership.groupId`
)
.join<TUsers>( .join<TUsers>(
db(TableName.Users).as("committerUser"), db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`, `${TableName.SecretApprovalRequest}.committerUserId`,
@@ -342,6 +425,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"), db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
// Bypasser fields
db.ref("bypasserUserId").withSchema(TableName.SecretApprovalPolicyBypasser),
db.ref("userId").withSchema("bypasserUserGroupMembership").as("bypasserGroupUserId"),
db.ref("email").withSchema("committerUser").as("committerUserEmail"), db.ref("email").withSchema("committerUser").as("committerUserEmail"),
db.ref("username").withSchema("committerUser").as("committerUserUsername"), db.ref("username").withSchema("committerUser").as("committerUserUsername"),
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"), db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
@@ -355,7 +443,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
.from<Awaited<typeof query>[number]>("w") .from<Awaited<typeof query>[number]>("w")
.where("w.rank", ">=", offset) .where("w.rank", ">=", offset)
.andWhere("w.rank", "<", offset + limit); .andWhere("w.rank", "<", offset + limit);
const formatedDoc = sqlNestRelationships({ const formattedDoc = sqlNestRelationships({
data: docs, data: docs,
key: "id", key: "id",
parentMapper: (el) => ({ parentMapper: (el) => ({
@@ -403,12 +491,22 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
key: "approverGroupUserId", key: "approverGroupUserId",
label: "approvers" as const, label: "approvers" as const,
mapper: ({ approverGroupUserId }) => ({ userId: approverGroupUserId }) mapper: ({ approverGroupUserId }) => ({ userId: approverGroupUserId })
},
{
key: "bypasserUserId",
label: "bypassers" as const,
mapper: ({ bypasserUserId }) => ({ userId: bypasserUserId })
},
{
key: "bypasserGroupUserId",
label: "bypassers" as const,
mapper: ({ bypasserGroupUserId }) => ({ userId: bypasserGroupUserId })
} }
] ]
}); });
return formatedDoc.map((el) => ({ return formattedDoc.map((el) => ({
...el, ...el,
policy: { ...el.policy, approvers: el.approvers } policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
})); }));
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "FindSAR" }); throw new DatabaseError({ error, name: "FindSAR" });
@@ -440,6 +538,16 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`, `${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId` `${TableName.UserGroupMembership}.groupId`
) )
.leftJoin(
TableName.SecretApprovalPolicyBypasser,
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyBypasser}.policyId`
)
.leftJoin<TUserGroupMembership>(
db(TableName.UserGroupMembership).as("bypasserUserGroupMembership"),
`${TableName.SecretApprovalPolicyBypasser}.bypasserGroupId`,
`bypasserUserGroupMembership.groupId`
)
.join<TUsers>( .join<TUsers>(
db(TableName.Users).as("committerUser"), db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`, `${TableName.SecretApprovalRequest}.committerUserId`,
@@ -491,6 +599,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"), db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
// Bypasser
db.ref("bypasserUserId").withSchema(TableName.SecretApprovalPolicyBypasser),
db.ref("userId").withSchema("bypasserUserGroupMembership").as("bypasserGroupUserId"),
db.ref("email").withSchema("committerUser").as("committerUserEmail"), db.ref("email").withSchema("committerUser").as("committerUserEmail"),
db.ref("username").withSchema("committerUser").as("committerUserUsername"), db.ref("username").withSchema("committerUser").as("committerUserUsername"),
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"), db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
@@ -504,7 +617,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
.from<Awaited<typeof query>[number]>("w") .from<Awaited<typeof query>[number]>("w")
.where("w.rank", ">=", offset) .where("w.rank", ">=", offset)
.andWhere("w.rank", "<", offset + limit); .andWhere("w.rank", "<", offset + limit);
const formatedDoc = sqlNestRelationships({ const formattedDoc = sqlNestRelationships({
data: docs, data: docs,
key: "id", key: "id",
parentMapper: (el) => ({ parentMapper: (el) => ({
@@ -554,12 +667,24 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
mapper: ({ approverGroupUserId }) => ({ mapper: ({ approverGroupUserId }) => ({
userId: approverGroupUserId userId: approverGroupUserId
}) })
},
{
key: "bypasserUserId",
label: "bypassers" as const,
mapper: ({ bypasserUserId }) => ({ userId: bypasserUserId })
},
{
key: "bypasserGroupUserId",
label: "bypassers" as const,
mapper: ({ bypasserGroupUserId }) => ({
userId: bypasserGroupUserId
})
} }
] ]
}); });
return formatedDoc.map((el) => ({ return formattedDoc.map((el) => ({
...el, ...el,
policy: { ...el.policy, approvers: el.approvers } policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
})); }));
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "FindSAR" }); throw new DatabaseError({ error, name: "FindSAR" });
@@ -62,11 +62,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns"; import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission";
ProjectPermissionApprovalActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "../permission/project-permission";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal"; import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service"; import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal"; import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
@@ -501,14 +497,14 @@ export const secretApprovalRequestServiceFactory = ({
}); });
} }
const { policy, folderId, projectId } = secretApprovalRequest; const { policy, folderId, projectId, bypassers } = secretApprovalRequest;
if (policy.deletedAt) { if (policy.deletedAt) {
throw new BadRequestError({ throw new BadRequestError({
message: "The policy associated with this secret approval request has been deleted." message: "The policy associated with this secret approval request has been deleted."
}); });
} }
const { hasRole, permission } = await permissionService.getProjectPermission({ const { hasRole } = await permissionService.getProjectPermission({
actor: ActorType.USER, actor: ActorType.USER,
actorId, actorId,
projectId, projectId,
@@ -534,14 +530,9 @@ export const secretApprovalRequestServiceFactory = ({
approverId ? reviewers[approverId] === ApprovalStatus.APPROVED : false approverId ? reviewers[approverId] === ApprovalStatus.APPROVED : false
).length; ).length;
const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft; const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft;
const canBypass = !bypassers.length || bypassers.some((bypasser) => bypasser.userId === actorId);
if ( if (!hasMinApproval && !(isSoftEnforcement && canBypass))
!hasMinApproval &&
!(
isSoftEnforcement &&
permission.can(ProjectPermissionApprovalActions.AllowChangeBypass, ProjectPermissionSub.SecretApproval)
)
)
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" }); throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
const { botKey, shouldUseSecretV2Bridge, project } = await projectBotService.getBotKey(projectId); const { botKey, shouldUseSecretV2Bridge, project } = await projectBotService.getBotKey(projectId);
+12 -2
View File
@@ -6,7 +6,10 @@ import { z } from "zod";
import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router"; import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router";
import { registerV1EERoutes } from "@app/ee/routes/v1"; import { registerV1EERoutes } from "@app/ee/routes/v1";
import { registerV2EERoutes } from "@app/ee/routes/v2"; import { registerV2EERoutes } from "@app/ee/routes/v2";
import { accessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal"; import {
accessApprovalPolicyApproverDALFactory,
accessApprovalPolicyBypasserDALFactory
} from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal";
import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal"; import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal";
import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service"; import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal"; import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal";
@@ -67,7 +70,10 @@ import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-d
import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service"; import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { scimDALFactory } from "@app/ee/services/scim/scim-dal"; import { scimDALFactory } from "@app/ee/services/scim/scim-dal";
import { scimServiceFactory } from "@app/ee/services/scim/scim-service"; import { scimServiceFactory } from "@app/ee/services/scim/scim-service";
import { secretApprovalPolicyApproverDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-approver-dal"; import {
secretApprovalPolicyApproverDALFactory,
secretApprovalPolicyBypasserDALFactory
} from "@app/ee/services/secret-approval-policy/secret-approval-policy-approver-dal";
import { secretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal"; import { secretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal";
import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { secretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { secretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
@@ -385,9 +391,11 @@ export const registerRoutes = async (
const accessApprovalPolicyDAL = accessApprovalPolicyDALFactory(db); const accessApprovalPolicyDAL = accessApprovalPolicyDALFactory(db);
const accessApprovalRequestDAL = accessApprovalRequestDALFactory(db); const accessApprovalRequestDAL = accessApprovalRequestDALFactory(db);
const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db); const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db);
const accessApprovalPolicyBypasserDAL = accessApprovalPolicyBypasserDALFactory(db);
const accessApprovalRequestReviewerDAL = accessApprovalRequestReviewerDALFactory(db); const accessApprovalRequestReviewerDAL = accessApprovalRequestReviewerDALFactory(db);
const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db); const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db);
const sapBypasserDAL = secretApprovalPolicyBypasserDALFactory(db);
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db); const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db); const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
const secretApprovalRequestReviewerDAL = secretApprovalRequestReviewerDALFactory(db); const secretApprovalRequestReviewerDAL = secretApprovalRequestReviewerDALFactory(db);
@@ -519,6 +527,7 @@ export const registerRoutes = async (
const secretApprovalPolicyService = secretApprovalPolicyServiceFactory({ const secretApprovalPolicyService = secretApprovalPolicyServiceFactory({
projectEnvDAL, projectEnvDAL,
secretApprovalPolicyApproverDAL: sapApproverDAL, secretApprovalPolicyApproverDAL: sapApproverDAL,
secretApprovalPolicyBypasserDAL: sapBypasserDAL,
permissionService, permissionService,
secretApprovalPolicyDAL, secretApprovalPolicyDAL,
licenseService, licenseService,
@@ -1218,6 +1227,7 @@ export const registerRoutes = async (
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({ const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
accessApprovalPolicyDAL, accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
accessApprovalPolicyBypasserDAL,
groupDAL, groupDAL,
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
@@ -2,7 +2,6 @@ export { useProjectPermission } from "./ProjectPermissionContext";
export type { ProjectPermissionSet, TProjectPermission } from "./types"; export type { ProjectPermissionSet, TProjectPermission } from "./types";
export { export {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionApprovalActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCmekActions, ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
@@ -24,15 +24,6 @@ export enum ProjectPermissionSecretActions {
Delete = "delete" Delete = "delete"
} }
export enum ProjectPermissionApprovalActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
AllowChangeBypass = "allow-change-bypass",
AllowAccessBypass = "allow-access-bypass"
}
export enum ProjectPermissionDynamicSecretActions { export enum ProjectPermissionDynamicSecretActions {
ReadRootCredential = "read-root-credential", ReadRootCredential = "read-root-credential",
CreateRootCredential = "create-root-credential", CreateRootCredential = "create-root-credential",
@@ -294,7 +285,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionApprovalActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ | [
ProjectPermissionIdentityActions, ProjectPermissionIdentityActions,
( (
-1
View File
@@ -10,7 +10,6 @@ export {
export type { TProjectPermission } from "./ProjectPermissionContext"; export type { TProjectPermission } from "./ProjectPermissionContext";
export { export {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionApprovalActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCmekActions, ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
@@ -21,6 +21,7 @@ export const useCreateAccessApprovalPolicy = () => {
projectSlug, projectSlug,
approvals, approvals,
approvers, approvers,
bypassers,
name, name,
secretPath, secretPath,
enforcementLevel, enforcementLevel,
@@ -30,6 +31,7 @@ export const useCreateAccessApprovalPolicy = () => {
environment, environment,
projectSlug, projectSlug,
approvals, approvals,
bypassers,
approvers, approvers,
secretPath, secretPath,
name, name,
@@ -53,6 +55,7 @@ export const useUpdateAccessApprovalPolicy = () => {
mutationFn: async ({ mutationFn: async ({
id, id,
approvers, approvers,
bypassers,
approvals, approvals,
name, name,
secretPath, secretPath,
@@ -62,6 +65,7 @@ export const useUpdateAccessApprovalPolicy = () => {
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, { const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
approvals, approvals,
approvers, approvers,
bypassers,
secretPath, secretPath,
name, name,
enforcementLevel, enforcementLevel,
@@ -16,6 +16,7 @@ export type TAccessApprovalPolicy = {
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
updatedAt: Date; updatedAt: Date;
approvers?: Approver[]; approvers?: Approver[];
bypassers?: Bypasser[];
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
}; };
@@ -24,11 +25,21 @@ export enum ApproverType {
Group = "group" Group = "group"
} }
export enum BypasserType {
User = "user",
Group = "group"
}
export type Approver = { export type Approver = {
id: string; id: string;
type: ApproverType; type: ApproverType;
}; };
export type Bypasser = {
id: string;
type: BypasserType;
};
export type TAccessApprovalRequest = { export type TAccessApprovalRequest = {
id: string; id: string;
policyId: string; policyId: string;
@@ -68,6 +79,7 @@ export type TAccessApprovalRequest = {
name: string; name: string;
approvals: number; approvals: number;
approvers: string[]; approvers: string[];
bypassers: string[];
secretPath?: string | null; secretPath?: string | null;
envId: string; envId: string;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
@@ -146,6 +158,7 @@ export type TCreateAccessPolicyDTO = {
name?: string; name?: string;
environment: string; environment: string;
approvers?: Approver[]; approvers?: Approver[];
bypassers?: Bypasser[];
approvals?: number; approvals?: number;
secretPath?: string; secretPath?: string;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
@@ -156,6 +169,7 @@ export type TUpdateAccessPolicyDTO = {
id: string; id: string;
name?: string; name?: string;
approvers?: Approver[]; approvers?: Approver[];
bypassers?: Bypasser[];
secretPath?: string; secretPath?: string;
environment?: string; environment?: string;
approvals?: number; approvals?: number;
@@ -14,6 +14,7 @@ export const useCreateSecretApprovalPolicy = () => {
workspaceId, workspaceId,
approvals, approvals,
approvers, approvers,
bypassers,
secretPath, secretPath,
name, name,
enforcementLevel, enforcementLevel,
@@ -24,6 +25,7 @@ export const useCreateSecretApprovalPolicy = () => {
workspaceId, workspaceId,
approvals, approvals,
approvers, approvers,
bypassers,
secretPath, secretPath,
name, name,
enforcementLevel, enforcementLevel,
@@ -46,6 +48,7 @@ export const useUpdateSecretApprovalPolicy = () => {
mutationFn: async ({ mutationFn: async ({
id, id,
approvers, approvers,
bypassers,
approvals, approvals,
secretPath, secretPath,
name, name,
@@ -55,6 +58,7 @@ export const useUpdateSecretApprovalPolicy = () => {
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, { const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
approvals, approvals,
approvers, approvers,
bypassers,
secretPath, secretPath,
name, name,
enforcementLevel, enforcementLevel,
@@ -25,6 +25,16 @@ export type Approver = {
type: ApproverType; type: ApproverType;
}; };
export enum BypasserType {
User = "user",
Group = "group"
}
export type Bypasser = {
id: string;
type: BypasserType;
};
export type TGetSecretApprovalPoliciesDTO = { export type TGetSecretApprovalPoliciesDTO = {
workspaceId: string; workspaceId: string;
}; };
@@ -41,6 +51,7 @@ export type TCreateSecretPolicyDTO = {
environment: string; environment: string;
secretPath?: string | null; secretPath?: string | null;
approvers?: Approver[]; approvers?: Approver[];
bypassers?: Bypasser[];
approvals?: number; approvals?: number;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
@@ -50,6 +61,7 @@ export type TUpdateSecretPolicyDTO = {
id: string; id: string;
name?: string; name?: string;
approvers?: Approver[]; approvers?: Approver[];
bypassers?: Bypasser[];
secretPath?: string | null; secretPath?: string | null;
approvals?: number; approvals?: number;
allowedSelfApprovals?: boolean; allowedSelfApprovals?: boolean;
@@ -57,7 +57,7 @@ export type TSecretApprovalRequest = {
secretPath: string; secretPath: string;
hasMerged: boolean; hasMerged: boolean;
status: "open" | "close"; status: "open" | "close";
policy: Omit<TSecretApprovalPolicy, "approvers"> & { policy: Omit<TSecretApprovalPolicy, "approvers" | "bypassers"> & {
approvers: { approvers: {
userId: string; userId: string;
email: string; email: string;
@@ -65,6 +65,13 @@ export type TSecretApprovalRequest = {
lastName: string; lastName: string;
username: string; username: string;
}[]; }[];
bypassers: {
userId: string;
email: string;
firstName: string;
lastName: string;
username: string;
}[];
}; };
statusChangedByUserId: string; statusChangedByUserId: string;
statusChangedByUser?: { statusChangedByUser?: {
@@ -12,7 +12,6 @@ import {
} from "@app/context"; } from "@app/context";
import { import {
PermissionConditionOperators, PermissionConditionOperators,
ProjectPermissionApprovalActions,
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
ProjectPermissionGroupActions, ProjectPermissionGroupActions,
ProjectPermissionIdentityActions, ProjectPermissionIdentityActions,
@@ -54,12 +53,10 @@ const SecretPolicyActionSchema = z.object({
}); });
const ApprovalPolicyActionSchema = z.object({ const ApprovalPolicyActionSchema = z.object({
[ProjectPermissionApprovalActions.Read]: z.boolean().optional(), [ProjectPermissionActions.Read]: z.boolean().optional(),
[ProjectPermissionApprovalActions.Edit]: z.boolean().optional(), [ProjectPermissionActions.Edit]: z.boolean().optional(),
[ProjectPermissionApprovalActions.Delete]: z.boolean().optional(), [ProjectPermissionActions.Delete]: z.boolean().optional(),
[ProjectPermissionApprovalActions.Create]: z.boolean().optional(), [ProjectPermissionActions.Create]: z.boolean().optional()
[ProjectPermissionApprovalActions.AllowChangeBypass]: z.boolean().optional(),
[ProjectPermissionApprovalActions.AllowAccessBypass]: z.boolean().optional()
}); });
const CmekPolicyActionSchema = z.object({ const CmekPolicyActionSchema = z.object({
@@ -574,24 +571,18 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
} }
if (subject === ProjectPermissionSub.SecretApproval) { if (subject === ProjectPermissionSub.SecretApproval) {
const canCreate = action.includes(ProjectPermissionApprovalActions.Create); const canCreate = action.includes(ProjectPermissionActions.Create);
const canDelete = action.includes(ProjectPermissionApprovalActions.Delete); const canDelete = action.includes(ProjectPermissionActions.Delete);
const canEdit = action.includes(ProjectPermissionApprovalActions.Edit); const canEdit = action.includes(ProjectPermissionActions.Edit);
const canRead = action.includes(ProjectPermissionApprovalActions.Read); const canRead = action.includes(ProjectPermissionActions.Read);
const canChangeBypass = action.includes(ProjectPermissionApprovalActions.AllowChangeBypass);
const canAccessBypass = action.includes(ProjectPermissionApprovalActions.AllowAccessBypass);
if (!formVal[subject]) formVal[subject] = [{}]; if (!formVal[subject]) formVal[subject] = [{}];
// Map actions to the keys defined in ApprovalPolicyActionSchema // Map actions to the keys defined in ApprovalPolicyActionSchema
if (canCreate) formVal[subject]![0][ProjectPermissionApprovalActions.Create] = true; if (canCreate) formVal[subject]![0][ProjectPermissionActions.Create] = true;
if (canDelete) formVal[subject]![0][ProjectPermissionApprovalActions.Delete] = true; if (canDelete) formVal[subject]![0][ProjectPermissionActions.Delete] = true;
if (canEdit) formVal[subject]![0][ProjectPermissionApprovalActions.Edit] = true; if (canEdit) formVal[subject]![0][ProjectPermissionActions.Edit] = true;
if (canRead) formVal[subject]![0][ProjectPermissionApprovalActions.Read] = true; if (canRead) formVal[subject]![0][ProjectPermissionActions.Read] = true;
if (canChangeBypass)
formVal[subject]![0][ProjectPermissionApprovalActions.AllowChangeBypass] = true;
if (canAccessBypass)
formVal[subject]![0][ProjectPermissionApprovalActions.AllowAccessBypass] = true;
return; return;
} }
@@ -1212,12 +1203,10 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.SecretApproval]: { [ProjectPermissionSub.SecretApproval]: {
title: "Secret Approval Policies", title: "Secret Approval Policies",
actions: [ actions: [
{ label: "Read", value: ProjectPermissionApprovalActions.Read }, { label: "Read", value: ProjectPermissionActions.Read },
{ label: "Create", value: ProjectPermissionApprovalActions.Create }, { label: "Create", value: ProjectPermissionActions.Create },
{ label: "Modify", value: ProjectPermissionApprovalActions.Edit }, { label: "Modify", value: ProjectPermissionActions.Edit },
{ label: "Remove", value: ProjectPermissionApprovalActions.Delete }, { label: "Remove", value: ProjectPermissionActions.Delete }
{ label: "Allow Change Bypass", value: ProjectPermissionApprovalActions.AllowChangeBypass },
{ label: "Allow Access Bypass", value: ProjectPermissionApprovalActions.AllowAccessBypass }
] ]
}, },
[ProjectPermissionSub.SecretRotation]: { [ProjectPermissionSub.SecretRotation]: {
@@ -1694,7 +1683,7 @@ export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
}, },
{ {
subject: ProjectPermissionSub.SecretApproval, subject: ProjectPermissionSub.SecretApproval,
actions: Object.values(ProjectPermissionApprovalActions) actions: Object.values(ProjectPermissionActions)
}, },
{ {
subject: ProjectPermissionSub.ServiceTokens, subject: ProjectPermissionSub.ServiceTokens,
@@ -25,7 +25,6 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge } from "@app/components/v2/Badge"; import { Badge } from "@app/components/v2/Badge";
import { import {
ProjectPermissionApprovalActions,
ProjectPermissionMemberActions, ProjectPermissionMemberActions,
ProjectPermissionSub, ProjectPermissionSub,
useProjectPermission, useProjectPermission,
@@ -102,11 +101,6 @@ export const AccessApprovalRequest = ({
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const canBypassApprovalPermission = permission.can(
ProjectPermissionApprovalActions.AllowAccessBypass,
ProjectPermissionSub.SecretApproval
);
const { data: members } = useGetWorkspaceUsers(projectId, true); const { data: members } = useGetWorkspaceUsers(projectId, true);
const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>( const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>(
(prev, curr) => ({ ...prev, [curr.user.id]: curr }), (prev, curr) => ({ ...prev, [curr.user.id]: curr }),
@@ -163,6 +157,8 @@ export const AccessApprovalRequest = ({
const isRequestedByCurrentUser = request.requestedByUserId === user.id; const isRequestedByCurrentUser = request.requestedByUserId === user.id;
const isSelfApproveAllowed = request.policy.allowedSelfApprovals; const isSelfApproveAllowed = request.policy.allowedSelfApprovals;
const userReviewStatus = request.reviewers.find(({ member }) => member === user.id)?.status; const userReviewStatus = request.reviewers.find(({ member }) => member === user.id)?.status;
const canBypass =
!request.policy.bypassers.length || request.policy.bypassers.includes(user.id);
let displayData: { label: string; type: "primary" | "danger" | "success" } = { let displayData: { label: string; type: "primary" | "danger" | "success" } = {
label: "", label: "",
@@ -198,6 +194,7 @@ export const AccessApprovalRequest = ({
userReviewStatus, userReviewStatus,
isAccepted, isAccepted,
isSoftEnforcement, isSoftEnforcement,
canBypass,
isRequestedByCurrentUser, isRequestedByCurrentUser,
isSelfApproveAllowed isSelfApproveAllowed
}; };
@@ -215,9 +212,7 @@ export const AccessApprovalRequest = ({
// Whether the current user can bypass policy // Whether the current user can bypass policy
const canBypass = const canBypass =
details.isSoftEnforcement && details.isSoftEnforcement && details.isRequestedByCurrentUser && details.canBypass;
details.isRequestedByCurrentUser &&
canBypassApprovalPermission;
// Whether the current user can approve // Whether the current user can approve
const canApprove = const canApprove =
@@ -240,14 +235,7 @@ export const AccessApprovalRequest = ({
handlePopUpOpen("reviewRequest"); handlePopUpOpen("reviewRequest");
}, },
[ [generateRequestDetails, membersGroupById, user, setSelectedRequest, handlePopUpOpen]
generateRequestDetails,
canBypassApprovalPermission,
membersGroupById,
user,
setSelectedRequest,
handlePopUpOpen
]
); );
return ( return (
@@ -471,7 +459,7 @@ export const AccessApprovalRequest = ({
setSelectedRequest(null); setSelectedRequest(null);
refetchRequests(); refetchRequests();
}} }}
canBypassApprovalPermission={canBypassApprovalPermission} canBypass={generateRequestDetails(selectedRequest).canBypass}
/> />
)} )}
@@ -19,7 +19,7 @@ export const ReviewAccessRequestModal = ({
projectSlug, projectSlug,
selectedRequester, selectedRequester,
selectedEnvSlug, selectedEnvSlug,
canBypassApprovalPermission canBypass
}: { }: {
isOpen: boolean; isOpen: boolean;
onOpenChange: (isOpen: boolean) => void; onOpenChange: (isOpen: boolean) => void;
@@ -32,7 +32,7 @@ export const ReviewAccessRequestModal = ({
projectSlug: string; projectSlug: string;
selectedRequester: string | undefined; selectedRequester: string | undefined;
selectedEnvSlug: string | undefined; selectedEnvSlug: string | undefined;
canBypassApprovalPermission: boolean; canBypass: boolean;
}) => { }) => {
const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null); const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null);
const [bypassApproval, setBypassApproval] = useState(false); const [bypassApproval, setBypassApproval] = useState(false);
@@ -208,7 +208,7 @@ export const ReviewAccessRequestModal = ({
{isSoftEnforcement && {isSoftEnforcement &&
request.isRequestedByCurrentUser && request.isRequestedByCurrentUser &&
!(request.isApprover && request.isSelfApproveAllowed) && !(request.isApprover && request.isSelfApproveAllowed) &&
canBypassApprovalPermission && ( canBypass && (
<div className="mt-2 flex flex-col space-y-2"> <div className="mt-2 flex flex-col space-y-2">
<Checkbox <Checkbox
onCheckedChange={(checked) => setBypassApproval(checked === true)} onCheckedChange={(checked) => setBypassApproval(checked === true)}
@@ -35,7 +35,7 @@ import {
useSubscription, useSubscription,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { ProjectPermissionApprovalActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionActions } from "@app/context/ProjectPermissionContext/types";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { import {
useDeleteAccessApprovalPolicy, useDeleteAccessApprovalPolicy,
@@ -61,10 +61,8 @@ const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?:
projectSlug: currentWorkspace?.slug as string, projectSlug: currentWorkspace?.slug as string,
options: { options: {
enabled: enabled:
permission.can( permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) &&
ProjectPermissionApprovalActions.Read, !!currentWorkspace?.slug
ProjectPermissionSub.SecretApproval
) && !!currentWorkspace?.slug
} }
} }
); );
@@ -73,10 +71,8 @@ const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?:
workspaceId: currentWorkspace?.id as string, workspaceId: currentWorkspace?.id as string,
options: { options: {
enabled: enabled:
permission.can( permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) &&
ProjectPermissionApprovalActions.Read, !!currentWorkspace?.id
ProjectPermissionSub.SecretApproval
) && !!currentWorkspace?.id
} }
} }
); );
@@ -164,7 +160,7 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
</div> </div>
<div> <div>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionApprovalActions.Create} I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.SecretApproval} a={ProjectPermissionSub.SecretApproval}
> >
{(isAllowed) => ( {(isAllowed) => (
@@ -27,7 +27,11 @@ import {
useCreateAccessApprovalPolicy, useCreateAccessApprovalPolicy,
useUpdateAccessApprovalPolicy useUpdateAccessApprovalPolicy
} from "@app/hooks/api/accessApproval"; } from "@app/hooks/api/accessApproval";
import { ApproverType, TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types"; import {
ApproverType,
BypasserType,
TAccessApprovalPolicy
} from "@app/hooks/api/accessApproval/types";
import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums"; import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums";
import { TWorkspaceUser } from "@app/hooks/api/users/types"; import { TWorkspaceUser } from "@app/hooks/api/users/types";
@@ -54,6 +58,14 @@ const formSchema = z
.object({ type: z.literal(ApproverType.Group), id: z.string() }) .object({ type: z.literal(ApproverType.Group), id: z.string() })
.array() .array()
.default([]), .default([]),
userBypassers: z
.object({ type: z.literal(BypasserType.User), id: z.string() })
.array()
.default([]),
groupBypassers: z
.object({ type: z.literal(BypasserType.Group), id: z.string() })
.array()
.default([]),
policyType: z.nativeEnum(PolicyType), policyType: z.nativeEnum(PolicyType),
enforcementLevel: z.nativeEnum(EnforcementLevel), enforcementLevel: z.nativeEnum(EnforcementLevel),
allowedSelfApprovals: z.boolean().default(true) allowedSelfApprovals: z.boolean().default(true)
@@ -103,6 +115,14 @@ export const AccessPolicyForm = ({
editValues?.approvers editValues?.approvers
?.filter((approver) => approver.type === ApproverType.Group) ?.filter((approver) => approver.type === ApproverType.Group)
.map(({ id, type }) => ({ id, type: type as ApproverType.Group })) || [], .map(({ id, type }) => ({ id, type: type as ApproverType.Group })) || [],
userBypassers:
editValues?.bypassers
?.filter((bypasser) => bypasser.type === BypasserType.User)
.map(({ id, type }) => ({ id, type: type as BypasserType.User })) || [],
groupBypassers:
editValues?.bypassers
?.filter((bypasser) => bypasser.type === BypasserType.Group)
.map(({ id, type }) => ({ id, type: type as BypasserType.Group })) || [],
approvals: editValues?.approvals, approvals: editValues?.approvals,
allowedSelfApprovals: editValues?.allowedSelfApprovals allowedSelfApprovals: editValues?.allowedSelfApprovals
} }
@@ -125,20 +145,30 @@ export const AccessPolicyForm = ({
const { mutateAsync: updateSecretApprovalPolicy } = useUpdateSecretApprovalPolicy(); const { mutateAsync: updateSecretApprovalPolicy } = useUpdateSecretApprovalPolicy();
const policyName = policyDetails[watch("policyType")]?.name || "Policy"; const policyName = policyDetails[watch("policyType")]?.name || "Policy";
const enforcementLevel = watch("enforcementLevel");
const formUserBypassers = watch("userBypassers");
const formGroupBypassers = watch("groupBypassers");
const bypasserCount = (formUserBypassers || []).length + (formGroupBypassers || []).length;
const handleCreatePolicy = async ({ const handleCreatePolicy = async ({
environment, environment,
groupApprovers, groupApprovers,
userApprovers, userApprovers,
groupBypassers,
userBypassers,
...data ...data
}: TFormSchema) => { }: TFormSchema) => {
if (!projectId) return; if (!projectId) return;
try { try {
const bypassers = [...userBypassers, ...groupBypassers];
if (data.policyType === PolicyType.ChangePolicy) { if (data.policyType === PolicyType.ChangePolicy) {
await createSecretApprovalPolicy({ await createSecretApprovalPolicy({
...data, ...data,
approvers: [...userApprovers, ...groupApprovers], approvers: [...userApprovers, ...groupApprovers],
bypassers: bypassers.length > 0 ? bypassers : undefined,
environment: environment.slug, environment: environment.slug,
workspaceId: currentWorkspace?.id || "" workspaceId: currentWorkspace?.id || ""
}); });
@@ -146,6 +176,7 @@ export const AccessPolicyForm = ({
await createAccessApprovalPolicy({ await createAccessApprovalPolicy({
...data, ...data,
approvers: [...userApprovers, ...groupApprovers], approvers: [...userApprovers, ...groupApprovers],
bypassers: bypassers.length > 0 ? bypassers : undefined,
environment: environment.slug, environment: environment.slug,
projectSlug projectSlug
}); });
@@ -168,17 +199,22 @@ export const AccessPolicyForm = ({
environment, environment,
userApprovers, userApprovers,
groupApprovers, groupApprovers,
userBypassers,
groupBypassers,
...data ...data
}: TFormSchema) => { }: TFormSchema) => {
if (!projectId || !projectSlug) return; if (!projectId || !projectSlug) return;
if (!editValues?.id) return; if (!editValues?.id) return;
try { try {
const bypassers = [...userBypassers, ...groupBypassers];
if (data.policyType === PolicyType.ChangePolicy) { if (data.policyType === PolicyType.ChangePolicy) {
await updateSecretApprovalPolicy({ await updateSecretApprovalPolicy({
id: editValues?.id, id: editValues?.id,
...data, ...data,
approvers: [...userApprovers, ...groupApprovers], approvers: [...userApprovers, ...groupApprovers],
bypassers: bypassers.length > 0 ? bypassers : undefined,
workspaceId: currentWorkspace?.id || "" workspaceId: currentWorkspace?.id || ""
}); });
} else { } else {
@@ -186,6 +222,7 @@ export const AccessPolicyForm = ({
id: editValues?.id, id: editValues?.id,
...data, ...data,
approvers: [...userApprovers, ...groupApprovers], approvers: [...userApprovers, ...groupApprovers],
bypassers: bypassers.length > 0 ? bypassers : undefined,
environment: environment.slug, environment: environment.slug,
projectSlug projectSlug
}); });
@@ -230,6 +267,24 @@ export const AccessPolicyForm = ({
[groups] [groups]
); );
const bypasserMemberOptions = useMemo(
() =>
members.map((member) => ({
id: member.user.id,
type: BypasserType.User
})),
[members]
);
const bypasserGroupOptions = useMemo(
() =>
groups?.map(({ group }) => ({
id: group.id,
type: BypasserType.Group
})),
[groups]
);
return ( return (
<Modal isOpen={isOpen} onOpenChange={onToggle}> <Modal isOpen={isOpen} onOpenChange={onToggle}>
<ModalContent <ModalContent
@@ -345,58 +400,62 @@ export const AccessPolicyForm = ({
Select members or groups that are allowed to approve requests from this policy. Select members or groups that are allowed to approve requests from this policy.
</p> </p>
</div> </div>
<Controller <div className="flex gap-2">
control={control} <Controller
name="userApprovers" control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => ( name="userApprovers"
<FormControl render={({ field: { value, onChange }, fieldState: { error } }) => (
label="User Approvers" <FormControl
isError={Boolean(error)} label="User Approvers"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<FilterableSelect className="w-1/2"
menuPlacement="top" >
isMulti <FilterableSelect
placeholder="Select members that are allowed to approve requests..." menuPlacement="top"
options={memberOptions} isMulti
getOptionValue={(option) => option.id} placeholder="Select members..."
getOptionLabel={(option) => { options={memberOptions}
const member = members?.find((m) => m.user.id === option.id); getOptionValue={(option) => option.id}
getOptionLabel={(option) => {
const member = members?.find((m) => m.user.id === option.id);
if (!member) return option.id; if (!member) return option.id;
return getMemberLabel(member); return getMemberLabel(member);
}} }}
value={value} value={value}
onChange={onChange} onChange={onChange}
/> />
</FormControl> </FormControl>
)} )}
/> />
<Controller <Controller
control={control} control={control}
name="groupApprovers" name="groupApprovers"
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl <FormControl
label="Group Approvers" label="Group Approvers"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
> className="w-1/2"
<FilterableSelect >
menuPlacement="top" <FilterableSelect
isMulti menuPlacement="top"
placeholder="Select groups that are allowed to approve requests..." isMulti
options={groupOptions} placeholder="Select groups..."
getOptionValue={(option) => option.id} options={groupOptions}
getOptionLabel={(option) => getOptionValue={(option) => option.id}
groups?.find(({ group }) => group.id === option.id)?.group.name ?? option.id getOptionLabel={(option) =>
} groups?.find(({ group }) => group.id === option.id)?.group.name ?? option.id
value={value} }
onChange={onChange} value={value}
/> onChange={onChange}
</FormControl> />
)} </FormControl>
/> )}
/>
</div>
<Controller <Controller
control={control} control={control}
name="allowedSelfApprovals" name="allowedSelfApprovals"
@@ -427,6 +486,7 @@ export const AccessPolicyForm = ({
label="Bypass Approvals" label="Bypass Approvals"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
className="mb-3"
> >
<Switch <Switch
id="bypass-approvals" id="bypass-approvals"
@@ -436,11 +496,78 @@ export const AccessPolicyForm = ({
onChange(v ? EnforcementLevel.Soft : EnforcementLevel.Hard) onChange(v ? EnforcementLevel.Soft : EnforcementLevel.Hard)
} }
> >
Allow request creators to bypass policy in break-glass situations Allow certain users to bypass policy in break-glass situations
</Switch> </Switch>
</FormControl> </FormControl>
)} )}
/> />
{enforcementLevel === EnforcementLevel.Soft && (
<>
<div className="flex gap-2">
<Controller
control={control}
name="userBypassers"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="User Bypassers"
isError={Boolean(error)}
errorText={error?.message}
className="mb-2 w-1/2"
>
<FilterableSelect
menuPlacement="top"
isMulti
placeholder="Select members..."
options={bypasserMemberOptions}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => {
const member = members?.find((m) => m.user.id === option.id);
if (!member) return option.id;
return getMemberLabel(member);
}}
value={value}
onChange={onChange}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="groupBypassers"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Group Bypassers"
isError={Boolean(error)}
errorText={error?.message}
className="mb-2 w-1/2"
>
<FilterableSelect
menuPlacement="top"
isMulti
placeholder="Select groups..."
options={bypasserGroupOptions}
getOptionValue={(option) => option.id}
getOptionLabel={(option) =>
groups?.find(({ group }) => group.id === option.id)?.group.name ??
option.id
}
value={value}
onChange={onChange}
/>
</FormControl>
)}
/>
</div>
{bypasserCount <= 0 && (
<span className="text-sm text-red-500">
Not selecting specific users or groups will allow anyone to bypass this policy
</span>
)}
</>
)}
<div className="mt-8 flex items-center space-x-4"> <div className="mt-8 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}> <Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
Save Save
@@ -15,7 +15,7 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge } from "@app/components/v2/Badge"; import { Badge } from "@app/components/v2/Badge";
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionApprovalActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionActions } from "@app/context/ProjectPermissionContext/types";
import { getMemberLabel } from "@app/helpers/members"; import { getMemberLabel } from "@app/helpers/members";
import { policyDetails } from "@app/helpers/policies"; import { policyDetails } from "@app/helpers/policies";
import { Approver } from "@app/hooks/api/accessApproval/types"; import { Approver } from "@app/hooks/api/accessApproval/types";
@@ -118,7 +118,7 @@ export const ApprovalPolicyRow = ({
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="center" className="min-w-[100%] p-1"> <DropdownMenuContent align="center" className="min-w-[100%] p-1">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionApprovalActions.Edit} I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.SecretApproval} a={ProjectPermissionSub.SecretApproval}
> >
{(isAllowed) => ( {(isAllowed) => (
@@ -137,7 +137,7 @@ export const ApprovalPolicyRow = ({
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionApprovalActions.Delete} I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.SecretApproval} a={ProjectPermissionSub.SecretApproval}
> >
{(isAllowed) => ( {(isAllowed) => (
@@ -13,11 +13,6 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, Checkbox, FormControl, Input } from "@app/components/v2"; import { Button, Checkbox, FormControl, Input } from "@app/components/v2";
import {
ProjectPermissionApprovalActions,
ProjectPermissionSub,
useProjectPermission
} from "@app/context";
import { import {
usePerformSecretApprovalRequestMerge, usePerformSecretApprovalRequestMerge,
useUpdateSecretApprovalRequestStatus useUpdateSecretApprovalRequestStatus
@@ -31,6 +26,7 @@ type Props = {
status: "close" | "open"; status: "close" | "open";
approvals: number; approvals: number;
canApprove?: boolean; canApprove?: boolean;
isBypasser: boolean;
statusChangeByEmail?: string; statusChangeByEmail?: string;
workspaceId: string; workspaceId: string;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
@@ -45,7 +41,8 @@ export const SecretApprovalRequestAction = ({
statusChangeByEmail, statusChangeByEmail,
workspaceId, workspaceId,
enforcementLevel, enforcementLevel,
canApprove canApprove,
isBypasser
}: Props) => { }: Props) => {
const { mutateAsync: performSecretApprovalMerge, isPending: isMerging } = const { mutateAsync: performSecretApprovalMerge, isPending: isMerging } =
usePerformSecretApprovalRequestMerge(); usePerformSecretApprovalRequestMerge();
@@ -53,12 +50,6 @@ export const SecretApprovalRequestAction = ({
const { mutateAsync: updateSecretStatusChange, isPending: isStatusChanging } = const { mutateAsync: updateSecretStatusChange, isPending: isStatusChanging } =
useUpdateSecretApprovalRequestStatus(); useUpdateSecretApprovalRequestStatus();
const { permission } = useProjectPermission();
const canBypassApprovalPermission = permission.can(
ProjectPermissionApprovalActions.AllowChangeBypass,
ProjectPermissionSub.SecretApproval
);
const [byPassApproval, setByPassApproval] = useState(false); const [byPassApproval, setByPassApproval] = useState(false);
const [bypassReason, setBypassReason] = useState(""); const [bypassReason, setBypassReason] = useState("");
@@ -134,10 +125,10 @@ export const SecretApprovalRequestAction = ({
)} )}
</span> </span>
</div> </div>
<div {isSoftEnforcement && !isMergable && isBypasser && (
className={`mt-4 w-full border-mineshaft-600 px-5 ${isMergable ? "border-t pb-2" : "border-y pb-4"}`} <div
> className={`mt-4 w-full border-mineshaft-600 px-5 ${isMergable ? "border-t pb-2" : "border-y pb-4"}`}
{isSoftEnforcement && !isMergable && canBypassApprovalPermission && ( >
<div className="mt-2 flex flex-col space-y-2 pt-2"> <div className="mt-2 flex flex-col space-y-2 pt-2">
<Checkbox <Checkbox
onCheckedChange={(checked) => setByPassApproval(checked === true)} onCheckedChange={(checked) => setByPassApproval(checked === true)}
@@ -169,8 +160,8 @@ export const SecretApprovalRequestAction = ({
</FormControl> </FormControl>
)} )}
</div> </div>
)} </div>
</div> )}
<div className="mt-2 flex w-full items-center justify-end space-x-2 px-4"> <div className="mt-2 flex w-full items-center justify-end space-x-2 px-4">
{canApprove || isSoftEnforcement ? ( {canApprove || isSoftEnforcement ? (
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
@@ -139,6 +139,11 @@ export const SecretApprovalRequestChanges = ({
({ userId }) => userId === userSession.id ({ userId }) => userId === userSession.id
); );
const isBypasser =
!secretApprovalRequestDetails?.policy?.bypassers ||
!secretApprovalRequestDetails.policy.bypassers.length ||
secretApprovalRequestDetails.policy.bypassers.some(({ userId }) => userId === userSession.id);
const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce< const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce<
Record<string, { status: ApprovalStatus; comment: string }> Record<string, { status: ApprovalStatus; comment: string }>
>( >(
@@ -414,6 +419,7 @@ export const SecretApprovalRequestChanges = ({
<div className="mt-2 flex items-center space-x-6 rounded-lg border border-mineshaft-600 bg-mineshaft-800"> <div className="mt-2 flex items-center space-x-6 rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<SecretApprovalRequestAction <SecretApprovalRequestAction
canApprove={canApprove} canApprove={canApprove}
isBypasser={isBypasser === undefined ? true : isBypasser}
approvalRequestId={secretApprovalRequestDetails.id} approvalRequestId={secretApprovalRequestDetails.id}
hasMerged={hasMerged} hasMerged={hasMerged}
approvals={secretApprovalRequestDetails.policy.approvals || 0} approvals={secretApprovalRequestDetails.policy.approvals || 0}