Merge pull request #3662 from Infisical/ENG-2800

feat(policies): Bypass Approval Rework
This commit is contained in:
x032205
2025-05-30 12:00:11 -04:00
committed by GitHub
47 changed files with 1217 additions and 278 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";
@@ -24,10 +24,19 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
approvers: z approvers: z
.discriminatedUnion("type", [ .discriminatedUnion("type", [
z.object({ type: z.literal(ApproverType.Group), id: z.string() }), z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() }) z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() })
]) ])
.array() .array()
.max(100, "Cannot have more than 100 approvers")
.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(), username: z.string().optional() })
])
.array()
.max(100, "Cannot have more than 100 bypassers")
.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 +81,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()
@@ -143,10 +153,19 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
approvers: z approvers: z
.discriminatedUnion("type", [ .discriminatedUnion("type", [
z.object({ type: z.literal(ApproverType.Group), id: z.string() }), z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() }) z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() })
]) ])
.array() .array()
.min(1, { message: "At least one approver should be provided" }), .min(1, { message: "At least one approver should be provided" })
.max(100, "Cannot have more than 100 approvers"),
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(), username: z.string().optional() })
])
.array()
.max(100, "Cannot have more than 100 bypassers")
.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 +239,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";
@@ -30,10 +30,19 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
approvers: z approvers: z
.discriminatedUnion("type", [ .discriminatedUnion("type", [
z.object({ type: z.literal(ApproverType.Group), id: z.string() }), z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() }) z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() })
]) ])
.array() .array()
.min(1, { message: "At least one approver should be provided" }), .min(1, { message: "At least one approver should be provided" })
.max(100, "Cannot have more than 100 approvers"),
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(), username: z.string().optional() })
])
.array()
.max(100, "Cannot have more than 100 bypassers")
.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)
@@ -75,10 +84,19 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
approvers: z approvers: z
.discriminatedUnion("type", [ .discriminatedUnion("type", [
z.object({ type: z.literal(ApproverType.Group), id: z.string() }), z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() }) z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() })
]) ])
.array() .array()
.min(1, { message: "At least one approver should be provided" }), .min(1, { message: "At least one approver should be provided" })
.max(100, "Cannot have more than 100 approvers"),
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(), username: z.string().optional() })
])
.array()
.max(100, "Cannot have more than 100 bypassers")
.optional(),
approvals: z.number().min(1).default(1), approvals: z.number().min(1).default(1),
secretPath: z secretPath: z
.string() .string()
@@ -157,6 +175,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()
@@ -193,7 +217,14 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
.object({ .object({
id: z.string().nullable().optional(), id: z.string().nullable().optional(),
type: z.nativeEnum(ApproverType), type: z.nativeEnum(ApproverType),
name: z.string().nullable().optional() username: z.string().nullable().optional()
})
.array(),
bypassers: z
.object({
id: z.string().nullable().optional(),
type: z.nativeEnum(BypasserType),
username: 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,8 +2,9 @@ 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 { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
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";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
@@ -14,10 +15,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,12 +37,14 @@ 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">;
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update" | "find">; accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update" | "find">;
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">; additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update">; accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find">;
}; };
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>; export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
@@ -45,6 +52,7 @@ export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprov
export const accessApprovalPolicyServiceFactory = ({ export const accessApprovalPolicyServiceFactory = ({
accessApprovalPolicyDAL, accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
accessApprovalPolicyBypasserDAL,
groupDAL, groupDAL,
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
@@ -52,7 +60,8 @@ export const accessApprovalPolicyServiceFactory = ({
userDAL, userDAL,
accessApprovalRequestDAL, accessApprovalRequestDAL,
additionalPrivilegeDAL, additionalPrivilegeDAL,
accessApprovalRequestReviewerDAL accessApprovalRequestReviewerDAL,
orgMembershipDAL
}: TAccessApprovalPolicyServiceFactoryDep) => { }: TAccessApprovalPolicyServiceFactoryDep) => {
const createAccessApprovalPolicy = async ({ const createAccessApprovalPolicy = async ({
name, name,
@@ -63,6 +72,7 @@ export const accessApprovalPolicyServiceFactory = ({
actorAuthMethod, actorAuthMethod,
approvals, approvals,
approvers, approvers,
bypassers,
projectSlug, projectSlug,
environment, environment,
enforcementLevel, enforcementLevel,
@@ -82,7 +92,7 @@ export const accessApprovalPolicyServiceFactory = ({
.filter(Boolean) as string[]; .filter(Boolean) as string[];
const userApproverNames = approvers const userApproverNames = approvers
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined)) .map((approver) => (approver.type === ApproverType.User ? approver.username : undefined))
.filter(Boolean) as string[]; .filter(Boolean) as string[];
if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length) if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length)
@@ -98,7 +108,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 +157,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.username : 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 +207,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 +228,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 +281,7 @@ export const accessApprovalPolicyServiceFactory = ({
const updateAccessApprovalPolicy = async ({ const updateAccessApprovalPolicy = async ({
policyId, policyId,
approvers, approvers,
bypassers,
secretPath, secretPath,
name, name,
actorId, actorId,
@@ -231,15 +302,15 @@ export const accessApprovalPolicyServiceFactory = ({
.filter(Boolean) as string[]; .filter(Boolean) as string[];
const userApproverNames = approvers const userApproverNames = approvers
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined)) .map((approver) => (approver.type === ApproverType.User ? approver.username : undefined))
.filter(Boolean) as string[]; .filter(Boolean) as string[];
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId); const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
const currentAppovals = approvals || accessApprovalPolicy.approvals; const currentApprovals = approvals || accessApprovalPolicy.approvals;
if ( if (
groupApprovers?.length === 0 && groupApprovers?.length === 0 &&
userApprovers && userApprovers &&
currentAppovals > userApprovers.length + userApproverNames.length currentApprovals > userApprovers.length + userApproverNames.length
) { ) {
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
} }
@@ -256,10 +327,79 @@ 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[];
groupBypassers = [...new Set(groupBypassers)];
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.username : 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 = [...new Set(bypasserUserIds.concat(bypasserUsers.map((user) => user.id)))];
}
// Validate user bypassers
if (bypasserUserIds.length > 0) {
const orgMemberships = await orgMembershipDAL.find({
$in: { userId: bypasserUserIds },
orgId: actorOrgId
});
if (orgMemberships.length !== bypasserUserIds.length) {
const foundUserIdsInOrg = new Set(orgMemberships.map((mem) => mem.userId));
const missingUserIds = bypasserUserIds.filter((id) => !foundUserIdsInOrg.has(id));
throw new BadRequestError({
message: `One or more specified bypasser users are not part of the organization or do not exist. Invalid or non-member user IDs: ${missingUserIds.join(", ")}`
});
}
}
// Validate group bypassers
if (groupBypassers.length > 0) {
const orgGroups = await groupDAL.find({
$in: { id: groupBypassers },
orgId: actorOrgId
});
if (orgGroups.length !== groupBypassers.length) {
const foundGroupIdsInOrg = new Set(orgGroups.map((group) => group.id));
const missingGroupIds = groupBypassers.filter((id) => !foundGroupIdsInOrg.has(id));
throw new BadRequestError({
message: `One or more specified bypasser groups are not part of the organization or do not exist. Invalid or non-member group IDs: ${missingGroupIds.join(", ")}`
});
}
}
}
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 +456,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 +506,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 +597,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,20 @@ 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; username?: string })[];
bypassers?: (
| { type: BypasserType.Group; id: string }
| { type: BypasserType.User; id?: string; username?: string }
)[];
projectSlug: string; projectSlug: string;
name: string; name: string;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
@@ -32,7 +41,11 @@ export type TCreateAccessApprovalPolicy = {
export type TUpdateAccessApprovalPolicy = { 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; username?: string })[];
bypassers?: (
| { type: BypasserType.Group; id: string }
| { type: BypasserType.User; id?: string; username?: 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,13 @@ 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 user is (not an approver OR cant self approve) AND can't bypass policy
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 +369,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"),
@@ -143,7 +179,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
label: "approvers" as const, label: "approvers" as const,
mapper: ({ approverUserId: id, approverUsername }) => ({ mapper: ({ approverUserId: id, approverUsername }) => ({
type: ApproverType.User, type: ApproverType.User,
name: approverUsername, username: approverUsername,
id id
}) })
}, },
@@ -155,6 +191,23 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
id id
}) })
}, },
{
key: "bypasserUserId",
label: "bypassers" as const,
mapper: ({ bypasserUserId: id, bypasserUsername }) => ({
type: BypasserType.User,
username: 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,
@@ -74,7 +80,7 @@ export const secretApprovalPolicyServiceFactory = ({
.filter(Boolean) as string[]; .filter(Boolean) as string[];
const userApproverNames = approvers const userApproverNames = approvers
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined)) .map((approver) => (approver.type === ApproverType.User ? approver.username : undefined))
.filter(Boolean) as string[]; .filter(Boolean) as string[];
if (!groupApprovers.length && approvals > approvers.length) if (!groupApprovers.length && approvals > approvers.length)
@@ -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.username : 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,
@@ -186,7 +252,7 @@ export const secretApprovalPolicyServiceFactory = ({
.filter(Boolean) as string[]; .filter(Boolean) as string[];
const userApproverNames = approvers const userApproverNames = approvers
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined)) .map((approver) => (approver.type === ApproverType.User ? approver.username : undefined))
.filter(Boolean) as string[]; .filter(Boolean) as string[];
const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId); const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
@@ -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.username : 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,16 @@
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; username?: string })[];
bypassers?: (
| { type: BypasserType.Group; id: string }
| { type: BypasserType.User; id?: string; username?: string }
)[];
projectId: string; projectId: string;
name: string; name: string;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
@@ -17,7 +21,11 @@ export type TUpdateSapDTO = {
secretPolicyId: string; secretPolicyId: string;
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; username?: string })[];
bypassers?: (
| { type: BypasserType.Group; id: string }
| { type: BypasserType.User; id?: string; username?: 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);
+14 -3
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,
@@ -1226,7 +1236,8 @@ export const registerRoutes = async (
userDAL, userDAL,
accessApprovalRequestDAL, accessApprovalRequestDAL,
additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL, additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL,
accessApprovalRequestReviewerDAL accessApprovalRequestReviewerDAL,
orgMembershipDAL
}); });
const accessApprovalRequestService = accessApprovalRequestServiceFactory({ const accessApprovalRequestService = accessApprovalRequestServiceFactory({
@@ -37,6 +37,10 @@ The enforcement level determines how strict the policy is. A **Hard** enforcemen
Enabling the "Bypass Approvals" toggle during policy creation will create a **Soft** enforcement level. Disabling the toggle makes the enforcement level **Hard**. Enabling the "Bypass Approvals" toggle during policy creation will create a **Soft** enforcement level. Disabling the toggle makes the enforcement level **Hard**.
</Note> </Note>
If you choose to allow approval bypasses (Soft Enforcement), you may select specific users or groups that can perform the bypass for that specific policy. Not choosing users or groups will allow anyone to bypass the policy.
A policy bypasser cannot bypass requests from others; the bypass action can only be performed by the request creator.
### Self approvals ### Self approvals
If the **Self Approvals** option is enabled, users who are designated as approvers on the policy can approve requests that they themselves have submitted. If the **Self Approvals** option is enabled, users who are designated as approvers on the policy can approve requests that they themselves have submitted.
@@ -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
@@ -11,7 +11,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?: {
@@ -112,7 +112,10 @@ export const PreviewSection = () => {
Get unlimited members, projects, RBAC, smart alerts, and so much more. Get unlimited members, projects, RBAC, smart alerts, and so much more.
</p> </p>
</div> </div>
<OrgPermissionCan I={OrgPermissionBillingActions.ManageBilling} a={OrgPermissionSubjects.Billing}> <OrgPermissionCan
I={OrgPermissionBillingActions.ManageBilling}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
onClick={() => handleUpgradeBtnClick()} onClick={() => handleUpgradeBtnClick()}
@@ -156,7 +159,10 @@ export const PreviewSection = () => {
}`} }`}
</p> </p>
{isInfisicalCloud() && ( {isInfisicalCloud() && (
<OrgPermissionCan I={OrgPermissionBillingActions.ManageBilling} a={OrgPermissionSubjects.Billing}> <OrgPermissionCan
I={OrgPermissionBillingActions.ManageBilling}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => ( {(isAllowed) => (
<button <button
type="button" type="button"
@@ -74,7 +74,10 @@ export const CompanyNameSection = () => {
name="name" name="name"
/> />
</div> </div>
<OrgPermissionCan I={OrgPermissionBillingActions.ManageBilling} a={OrgPermissionSubjects.Billing}> <OrgPermissionCan
I={OrgPermissionBillingActions.ManageBilling}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
type="submit" type="submit"
@@ -75,7 +75,10 @@ export const InvoiceEmailSection = () => {
name="email" name="email"
/> />
</div> </div>
<OrgPermissionCan I={OrgPermissionBillingActions.ManageBilling} a={OrgPermissionSubjects.Billing}> <OrgPermissionCan
I={OrgPermissionBillingActions.ManageBilling}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
type="submit" type="submit"
@@ -27,7 +27,10 @@ export const PmtMethodsSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-8 flex items-center"> <div className="mb-8 flex items-center">
<h2 className="flex-1 text-xl font-semibold text-white">Payment methods</h2> <h2 className="flex-1 text-xl font-semibold text-white">Payment methods</h2>
<OrgPermissionCan I={OrgPermissionBillingActions.ManageBilling} a={OrgPermissionSubjects.Billing}> <OrgPermissionCan
I={OrgPermissionBillingActions.ManageBilling}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
onClick={handleAddPmtMethodBtnClick} onClick={handleAddPmtMethodBtnClick}
@@ -18,7 +18,10 @@ export const TaxIDSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-8 flex items-center"> <div className="mb-8 flex items-center">
<h2 className="flex-1 text-xl font-semibold text-white">Tax ID</h2> <h2 className="flex-1 text-xl font-semibold text-white">Tax ID</h2>
<OrgPermissionCan I={OrgPermissionBillingActions.ManageBilling} a={OrgPermissionSubjects.Billing}> <OrgPermissionCan
I={OrgPermissionBillingActions.ManageBilling}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
onClick={() => handlePopUpOpen("addTaxID")} onClick={() => handlePopUpOpen("addTaxID")}
@@ -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).default(EnforcementLevel.Hard), enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
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,6 +400,7 @@ 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>
<div className="flex gap-2">
<Controller <Controller
control={control} control={control}
name="userApprovers" name="userApprovers"
@@ -353,11 +409,12 @@ export const AccessPolicyForm = ({
label="User Approvers" label="User Approvers"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
className="w-1/2"
> >
<FilterableSelect <FilterableSelect
menuPlacement="top" menuPlacement="top"
isMulti isMulti
placeholder="Select members that are allowed to approve requests..." placeholder="Select members..."
options={memberOptions} options={memberOptions}
getOptionValue={(option) => option.id} getOptionValue={(option) => option.id}
getOptionLabel={(option) => { getOptionLabel={(option) => {
@@ -381,11 +438,12 @@ export const AccessPolicyForm = ({
label="Group Approvers" label="Group Approvers"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
className="w-1/2"
> >
<FilterableSelect <FilterableSelect
menuPlacement="top" menuPlacement="top"
isMulti isMulti
placeholder="Select groups that are allowed to approve requests..." placeholder="Select groups..."
options={groupOptions} options={groupOptions}
getOptionValue={(option) => option.id} getOptionValue={(option) => option.id}
getOptionLabel={(option) => getOptionLabel={(option) =>
@@ -397,6 +455,7 @@ export const AccessPolicyForm = ({
</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>
{isSoftEnforcement && !isMergable && isBypasser && (
<div <div
className={`mt-4 w-full border-mineshaft-600 px-5 ${isMergable ? "border-t pb-2" : "border-y pb-4"}`} 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}