mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 11:28:18 +00:00
Merge pull request #3662 from Infisical/ENG-2800
feat(policies): Bypass Approval Rework
This commit is contained in:
Vendored
+17
@@ -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>
|
||||||
|
>;
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -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?: {
|
||||||
|
|||||||
+8
-2
@@ -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"
|
||||||
|
|||||||
+4
-1
@@ -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"
|
||||||
|
|||||||
+4
-1
@@ -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"
|
||||||
|
|||||||
+4
-1
@@ -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}
|
||||||
|
|||||||
+4
-1
@@ -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")}
|
||||||
|
|||||||
+17
-28
@@ -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,
|
||||||
|
|||||||
+6
-18
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -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)}
|
||||||
|
|||||||
+6
-10
@@ -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) => (
|
||||||
|
|||||||
+179
-52
@@ -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,58 +400,62 @@ export const AccessPolicyForm = ({
|
|||||||
Select members or groups that are allowed to approve requests from this policy.
|
Select members or groups that are allowed to approve requests from this policy.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Controller
|
<div className="flex gap-2">
|
||||||
control={control}
|
<Controller
|
||||||
name="userApprovers"
|
control={control}
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
name="userApprovers"
|
||||||
<FormControl
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
label="User Approvers"
|
<FormControl
|
||||||
isError={Boolean(error)}
|
label="User Approvers"
|
||||||
errorText={error?.message}
|
isError={Boolean(error)}
|
||||||
>
|
errorText={error?.message}
|
||||||
<FilterableSelect
|
className="w-1/2"
|
||||||
menuPlacement="top"
|
>
|
||||||
isMulti
|
<FilterableSelect
|
||||||
placeholder="Select members that are allowed to approve requests..."
|
menuPlacement="top"
|
||||||
options={memberOptions}
|
isMulti
|
||||||
getOptionValue={(option) => option.id}
|
placeholder="Select members..."
|
||||||
getOptionLabel={(option) => {
|
options={memberOptions}
|
||||||
const member = members?.find((m) => m.user.id === option.id);
|
getOptionValue={(option) => option.id}
|
||||||
|
getOptionLabel={(option) => {
|
||||||
|
const member = members?.find((m) => m.user.id === option.id);
|
||||||
|
|
||||||
if (!member) return option.id;
|
if (!member) return option.id;
|
||||||
|
|
||||||
return getMemberLabel(member);
|
return getMemberLabel(member);
|
||||||
}}
|
}}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="groupApprovers"
|
name="groupApprovers"
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Group Approvers"
|
label="Group Approvers"
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
className="w-1/2"
|
||||||
<FilterableSelect
|
>
|
||||||
menuPlacement="top"
|
<FilterableSelect
|
||||||
isMulti
|
menuPlacement="top"
|
||||||
placeholder="Select groups that are allowed to approve requests..."
|
isMulti
|
||||||
options={groupOptions}
|
placeholder="Select groups..."
|
||||||
getOptionValue={(option) => option.id}
|
options={groupOptions}
|
||||||
getOptionLabel={(option) =>
|
getOptionValue={(option) => option.id}
|
||||||
groups?.find(({ group }) => group.id === option.id)?.group.name ?? option.id
|
getOptionLabel={(option) =>
|
||||||
}
|
groups?.find(({ group }) => group.id === option.id)?.group.name ?? option.id
|
||||||
value={value}
|
}
|
||||||
onChange={onChange}
|
value={value}
|
||||||
/>
|
onChange={onChange}
|
||||||
</FormControl>
|
/>
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="allowedSelfApprovals"
|
name="allowedSelfApprovals"
|
||||||
@@ -427,6 +486,7 @@ export const AccessPolicyForm = ({
|
|||||||
label="Bypass Approvals"
|
label="Bypass Approvals"
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
|
className="mb-3"
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
id="bypass-approvals"
|
id="bypass-approvals"
|
||||||
@@ -436,11 +496,78 @@ export const AccessPolicyForm = ({
|
|||||||
onChange(v ? EnforcementLevel.Soft : EnforcementLevel.Hard)
|
onChange(v ? EnforcementLevel.Soft : EnforcementLevel.Hard)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Allow request creators to bypass policy in break-glass situations
|
Allow certain users to bypass policy in break-glass situations
|
||||||
</Switch>
|
</Switch>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{enforcementLevel === EnforcementLevel.Soft && (
|
||||||
|
<>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="userBypassers"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="User Bypassers"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-2 w-1/2"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
menuPlacement="top"
|
||||||
|
isMulti
|
||||||
|
placeholder="Select members..."
|
||||||
|
options={bypasserMemberOptions}
|
||||||
|
getOptionValue={(option) => option.id}
|
||||||
|
getOptionLabel={(option) => {
|
||||||
|
const member = members?.find((m) => m.user.id === option.id);
|
||||||
|
|
||||||
|
if (!member) return option.id;
|
||||||
|
|
||||||
|
return getMemberLabel(member);
|
||||||
|
}}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="groupBypassers"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Group Bypassers"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-2 w-1/2"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
menuPlacement="top"
|
||||||
|
isMulti
|
||||||
|
placeholder="Select groups..."
|
||||||
|
options={bypasserGroupOptions}
|
||||||
|
getOptionValue={(option) => option.id}
|
||||||
|
getOptionLabel={(option) =>
|
||||||
|
groups?.find(({ group }) => group.id === option.id)?.group.name ??
|
||||||
|
option.id
|
||||||
|
}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{bypasserCount <= 0 && (
|
||||||
|
<span className="text-sm text-red-500">
|
||||||
|
Not selecting specific users or groups will allow anyone to bypass this policy
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className="mt-8 flex items-center space-x-4">
|
<div className="mt-8 flex items-center space-x-4">
|
||||||
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
|
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
|
||||||
Save
|
Save
|
||||||
|
|||||||
+3
-3
@@ -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) => (
|
||||||
|
|||||||
+9
-18
@@ -13,11 +13,6 @@ import { twMerge } from "tailwind-merge";
|
|||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, Checkbox, FormControl, Input } from "@app/components/v2";
|
import { Button, Checkbox, FormControl, Input } from "@app/components/v2";
|
||||||
import {
|
|
||||||
ProjectPermissionApprovalActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
useProjectPermission
|
|
||||||
} from "@app/context";
|
|
||||||
import {
|
import {
|
||||||
usePerformSecretApprovalRequestMerge,
|
usePerformSecretApprovalRequestMerge,
|
||||||
useUpdateSecretApprovalRequestStatus
|
useUpdateSecretApprovalRequestStatus
|
||||||
@@ -31,6 +26,7 @@ type Props = {
|
|||||||
status: "close" | "open";
|
status: "close" | "open";
|
||||||
approvals: number;
|
approvals: number;
|
||||||
canApprove?: boolean;
|
canApprove?: boolean;
|
||||||
|
isBypasser: boolean;
|
||||||
statusChangeByEmail?: string;
|
statusChangeByEmail?: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
@@ -45,7 +41,8 @@ export const SecretApprovalRequestAction = ({
|
|||||||
statusChangeByEmail,
|
statusChangeByEmail,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
enforcementLevel,
|
enforcementLevel,
|
||||||
canApprove
|
canApprove,
|
||||||
|
isBypasser
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { mutateAsync: performSecretApprovalMerge, isPending: isMerging } =
|
const { mutateAsync: performSecretApprovalMerge, isPending: isMerging } =
|
||||||
usePerformSecretApprovalRequestMerge();
|
usePerformSecretApprovalRequestMerge();
|
||||||
@@ -53,12 +50,6 @@ export const SecretApprovalRequestAction = ({
|
|||||||
const { mutateAsync: updateSecretStatusChange, isPending: isStatusChanging } =
|
const { mutateAsync: updateSecretStatusChange, isPending: isStatusChanging } =
|
||||||
useUpdateSecretApprovalRequestStatus();
|
useUpdateSecretApprovalRequestStatus();
|
||||||
|
|
||||||
const { permission } = useProjectPermission();
|
|
||||||
const canBypassApprovalPermission = permission.can(
|
|
||||||
ProjectPermissionApprovalActions.AllowChangeBypass,
|
|
||||||
ProjectPermissionSub.SecretApproval
|
|
||||||
);
|
|
||||||
|
|
||||||
const [byPassApproval, setByPassApproval] = useState(false);
|
const [byPassApproval, setByPassApproval] = useState(false);
|
||||||
const [bypassReason, setBypassReason] = useState("");
|
const [bypassReason, setBypassReason] = useState("");
|
||||||
|
|
||||||
@@ -134,10 +125,10 @@ export const SecretApprovalRequestAction = ({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
{isSoftEnforcement && !isMergable && isBypasser && (
|
||||||
className={`mt-4 w-full border-mineshaft-600 px-5 ${isMergable ? "border-t pb-2" : "border-y pb-4"}`}
|
<div
|
||||||
>
|
className={`mt-4 w-full border-mineshaft-600 px-5 ${isMergable ? "border-t pb-2" : "border-y pb-4"}`}
|
||||||
{isSoftEnforcement && !isMergable && canBypassApprovalPermission && (
|
>
|
||||||
<div className="mt-2 flex flex-col space-y-2 pt-2">
|
<div className="mt-2 flex flex-col space-y-2 pt-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
onCheckedChange={(checked) => setByPassApproval(checked === true)}
|
onCheckedChange={(checked) => setByPassApproval(checked === true)}
|
||||||
@@ -169,8 +160,8 @@ export const SecretApprovalRequestAction = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div className="mt-2 flex w-full items-center justify-end space-x-2 px-4">
|
<div className="mt-2 flex w-full items-center justify-end space-x-2 px-4">
|
||||||
{canApprove || isSoftEnforcement ? (
|
{canApprove || isSoftEnforcement ? (
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
|
|||||||
+6
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user