Merge pull request #2458 from meetcshah19/meet/eng-1443-add-groups-as-eligible-approvers

feat: allow access approvals with user groups
This commit is contained in:
Meet Shah
2024-09-23 19:14:52 +05:30
committed by GitHub
23 changed files with 728 additions and 179 deletions
@@ -1,6 +1,7 @@
import { seedData1 } from "@app/db/seed-data"; import { seedData1 } from "@app/db/seed-data";
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
const createPolicy = async (dto: { name: string; secretPath: string; approvers: string[]; approvals: number }) => { const createPolicy = async (dto: { name: string; secretPath: string; approvers: {type: ApproverType.User, id: string}[]; approvals: number }) => {
const res = await testServer.inject({ const res = await testServer.inject({
method: "POST", method: "POST",
url: `/api/v1/secret-approvals`, url: `/api/v1/secret-approvals`,
@@ -26,7 +27,7 @@ describe("Secret approval policy router", async () => {
const policy = await createPolicy({ const policy = await createPolicy({
secretPath: "/", secretPath: "/",
approvals: 1, approvals: 1,
approvers: [seedData1.id], approvers: [{id:seedData1.id, type: ApproverType.User}],
name: "test-policy" name: "test-policy"
}); });
@@ -0,0 +1,36 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
// add column approverGroupId to AccessApprovalPolicyApprover
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
// make nullable
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE");
// make approverUserId nullable
table.uuid("approverUserId").nullable().alter();
});
// add column approverGroupId to SecretApprovalPolicyApprover
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
table.uuid("approverGroupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
table.uuid("approverUserId").nullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
// remove
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
table.dropColumn("approverGroupId");
table.uuid("approverUserId").notNullable().alter();
});
// remove
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
table.dropColumn("approverGroupId");
table.uuid("approverUserId").notNullable().alter();
});
}
}
@@ -12,7 +12,8 @@ export const AccessApprovalPoliciesApproversSchema = z.object({
policyId: z.string().uuid(), policyId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
approverUserId: z.string().uuid() approverUserId: z.string().uuid().nullable().optional(),
approverGroupId: z.string().uuid().nullable().optional()
}); });
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>; export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;
@@ -12,7 +12,8 @@ export const SecretApprovalPoliciesApproversSchema = z.object({
policyId: z.string().uuid(), policyId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
approverUserId: z.string().uuid() approverUserId: z.string().uuid().nullable().optional(),
approverGroupId: z.string().uuid().nullable().optional()
}); });
export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>; export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>;
@@ -1,6 +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 { EnforcementLevel } from "@app/lib/types"; import { EnforcementLevel } from "@app/lib/types";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas"; import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
@@ -11,20 +12,18 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
url: "/", url: "/",
method: "POST", method: "POST",
schema: { schema: {
body: z body: z.object({
.object({ projectSlug: z.string().trim(),
projectSlug: z.string().trim(), name: z.string().optional(),
name: z.string().optional(), secretPath: z.string().trim().default("/"),
secretPath: z.string().trim().default("/"), environment: z.string(),
environment: z.string(), approvers: z
approvers: z.string().array().min(1), .object({ type: z.nativeEnum(ApproverType), id: z.string() })
approvals: z.number().min(1).default(1), .array()
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) .min(1, { message: "At least one approver should be provided" }),
}) approvals: z.number().min(1).default(1),
.refine((data) => data.approvals <= data.approvers.length, { enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
path: ["approvals"], }),
message: "The number of approvals should be lower than the number of approvers."
}),
response: { response: {
200: z.object({ 200: z.object({
approval: sapPubSchema approval: sapPubSchema
@@ -58,14 +57,15 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
200: z.object({ 200: z.object({
approvals: sapPubSchema approvals: sapPubSchema
.extend({ .extend({
userApprovers: z approvers: z
.object({ .object({ type: z.nativeEnum(ApproverType), id: z.string().nullable().optional() })
userId: z.string() .array()
}) .nullable()
.array(), .optional()
secretPath: z.string().optional().nullable()
}) })
.array() .array()
.nullable()
.optional()
}) })
} }
}, },
@@ -119,22 +119,20 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
params: z.object({ params: z.object({
policyId: z.string() policyId: z.string()
}), }),
body: z body: z.object({
.object({ name: z.string().optional(),
name: z.string().optional(), secretPath: z
secretPath: z .string()
.string() .trim()
.trim() .optional()
.optional() .transform((val) => (val === "" ? "/" : val)),
.transform((val) => (val === "" ? "/" : val)), approvers: z
approvers: z.string().array().min(1), .object({ type: z.nativeEnum(ApproverType), id: z.string() })
approvals: z.number().min(1).default(1), .array()
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) .min(1, { message: "At least one approver should be provided" }),
}) approvals: z.number().min(1).optional(),
.refine((data) => data.approvals <= data.approvers.length, { enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
path: ["approvals"], }),
message: "The number of approvals should be lower than the number of approvers."
}),
response: { response: {
200: z.object({ 200: z.object({
approval: sapPubSchema approval: sapPubSchema
@@ -1,6 +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 { 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";
@@ -16,25 +17,23 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
body: z body: z.object({
.object({ workspaceId: z.string(),
workspaceId: z.string(), name: z.string().optional(),
name: z.string().optional(), environment: z.string(),
environment: z.string(), secretPath: z
secretPath: z .string()
.string() .optional()
.optional() .nullable()
.nullable() .default("/")
.default("/") .transform((val) => (val ? removeTrailingSlash(val) : val)),
.transform((val) => (val ? removeTrailingSlash(val) : val)), approvers: z
approvers: z.string().array().min(1), .object({ type: z.nativeEnum(ApproverType), id: z.string() })
approvals: z.number().min(1).default(1), .array()
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) .min(1, { message: "At least one approver should be provided" }),
}) approvals: z.number().min(1).default(1),
.refine((data) => data.approvals <= data.approvers.length, { enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
path: ["approvals"], }),
message: "The number of approvals should be lower than the number of approvers."
}),
response: { response: {
200: z.object({ 200: z.object({
approval: sapPubSchema approval: sapPubSchema
@@ -67,23 +66,21 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
params: z.object({ params: z.object({
sapId: z.string() sapId: z.string()
}), }),
body: z body: z.object({
.object({ name: z.string().optional(),
name: z.string().optional(), approvers: z
approvers: z.string().array().min(1), .object({ type: z.nativeEnum(ApproverType), id: z.string() })
approvals: z.number().min(1).default(1), .array()
secretPath: z .min(1, { message: "At least one approver should be provided" }),
.string() approvals: z.number().min(1).default(1),
.optional() secretPath: z
.nullable() .string()
.transform((val) => (val ? removeTrailingSlash(val) : val)) .optional()
.transform((val) => (val === "" ? "/" : val)), .nullable()
enforcementLevel: z.nativeEnum(EnforcementLevel).optional() .transform((val) => (val ? removeTrailingSlash(val) : val))
}) .transform((val) => (val === "" ? "/" : val)),
.refine((data) => data.approvals <= data.approvers.length, { enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
path: ["approvals"], }),
message: "The number of approvals should be lower than the number of approvers."
}),
response: { response: {
200: z.object({ 200: z.object({
approval: sapPubSchema approval: sapPubSchema
@@ -147,9 +144,10 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
200: z.object({ 200: z.object({
approvals: sapPubSchema approvals: sapPubSchema
.extend({ .extend({
userApprovers: z approvers: z
.object({ .object({
userId: z.string() id: z.string().nullable().optional(),
type: z.nativeEnum(ApproverType)
}) })
.array() .array()
}) })
@@ -186,7 +184,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
200: z.object({ 200: z.object({
policy: sapPubSchema policy: sapPubSchema
.extend({ .extend({
userApprovers: z.object({ userId: z.string() }).array() userApprovers: z.object({ userId: z.string().nullable().optional() }).array()
}) })
.optional() .optional()
}) })
@@ -13,7 +13,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
const approvalRequestUser = z.object({ userId: z.string() }).merge( const approvalRequestUser = z.object({ userId: z.string().nullable().optional() }).merge(
UsersSchema.pick({ UsersSchema.pick({
email: true, email: true,
firstName: true, firstName: true,
@@ -46,7 +46,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
approvals: z.number(), approvals: z.number(),
approvers: z.string().array(), approvers: z
.object({
userId: z.string().nullable().optional()
})
.array(),
secretPath: z.string().optional().nullable(), secretPath: z.string().optional().nullable(),
enforcementLevel: z.string() enforcementLevel: z.string()
}), }),
@@ -54,7 +58,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(), commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
environment: z.string(), environment: z.string(),
reviewers: z.object({ userId: z.string(), status: z.string() }).array(), reviewers: z.object({ userId: z.string(), status: z.string() }).array(),
approvers: z.string().array() approvers: z
.object({
userId: z.string().nullable().optional()
})
.array()
}).array() }).array()
}) })
} }
@@ -5,6 +5,8 @@ import { AccessApprovalPoliciesSchema, TableName, TAccessApprovalPolicies } from
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";
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>; export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
export const accessApprovalPolicyDALFactory = (db: TDbClient) => { export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
@@ -21,6 +23,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
`${TableName.AccessApprovalPolicyApprover}.policyId` `${TableName.AccessApprovalPolicyApprover}.policyId`
) )
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover)) .select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("approverGroupId").withSchema(TableName.AccessApprovalPolicyApprover))
.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"))
@@ -30,10 +33,10 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
return result; return result;
}; };
const findById = async (id: string, tx?: Knex) => { const findById = async (policyId: string, tx?: Knex) => {
try { try {
const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), { const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), {
[`${TableName.AccessApprovalPolicy}.id` as "id"]: id [`${TableName.AccessApprovalPolicy}.id` as "id"]: policyId
}); });
const formattedDoc = sqlNestRelationships({ const formattedDoc = sqlNestRelationships({
data: doc, data: doc,
@@ -50,9 +53,18 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
childrenMapper: [ childrenMapper: [
{ {
key: "approverUserId", key: "approverUserId",
label: "userApprovers" as const, label: "approvers" as const,
mapper: ({ approverUserId }) => ({ mapper: ({ approverUserId: id }) => ({
userId: approverUserId id,
type: "user"
})
},
{
key: "approverGroupId",
label: "approvers" as const,
mapper: ({ approverGroupId: id }) => ({
id,
type: "group"
}) })
} }
] ]
@@ -84,9 +96,18 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
childrenMapper: [ childrenMapper: [
{ {
key: "approverUserId", key: "approverUserId",
label: "userApprovers" as const, label: "approvers" as const,
mapper: ({ approverUserId }) => ({ mapper: ({ approverUserId: id }) => ({
userId: approverUserId id,
type: ApproverType.User
})
},
{
key: "approverGroupId",
label: "approvers" as const,
mapper: ({ approverGroupId: id }) => ({
id,
type: ApproverType.Group
}) })
} }
] ]
@@ -7,10 +7,12 @@ 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";
import { TGroupDALFactory } from "../group/group-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal"; import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
import { verifyApprovers } from "./access-approval-policy-fns"; import { verifyApprovers } from "./access-approval-policy-fns";
import { import {
ApproverType,
TCreateAccessApprovalPolicy, TCreateAccessApprovalPolicy,
TDeleteAccessApprovalPolicy, TDeleteAccessApprovalPolicy,
TGetAccessPolicyCountByEnvironmentDTO, TGetAccessPolicyCountByEnvironmentDTO,
@@ -25,6 +27,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">;
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory; accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
groupDAL: TGroupDALFactory;
}; };
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>; export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
@@ -32,6 +35,7 @@ export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprov
export const accessApprovalPolicyServiceFactory = ({ export const accessApprovalPolicyServiceFactory = ({
accessApprovalPolicyDAL, accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
groupDAL,
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
projectDAL projectDAL
@@ -52,7 +56,15 @@ export const accessApprovalPolicyServiceFactory = ({
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new BadRequestError({ message: "Project not found" });
if (approvals > approvers.length) // If there is a group approver people might be added to the group later to meet the approvers quota
const groupApprovers = approvers
.filter((approver) => approver.type === ApproverType.Group)
.map((approver) => approver.id);
const userApprovers = approvers
.filter((approver) => approver.type === ApproverType.User)
.map((approver) => approver.id);
if (!groupApprovers && approvals > userApprovers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
@@ -69,6 +81,24 @@ export const accessApprovalPolicyServiceFactory = ({
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id }); const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
if (!env) throw new BadRequestError({ message: "Environment not found" }); if (!env) throw new BadRequestError({ message: "Environment not found" });
const verifyAllApprovers = userApprovers;
const usersPromises: Promise<
{
id: string;
email: string | null | undefined;
username: string;
firstName: string | null | undefined;
lastName: string | null | undefined;
isPartOfGroup: boolean;
}[]
>[] = [];
for (const groupId of groupApprovers) {
usersPromises.push(groupDAL.findAllGroupMembers({ orgId: actorOrgId, groupId, offset: 0 }));
}
const verifyGroupApprovers = (await Promise.all(usersPromises)).flat().map((user) => user.id);
verifyAllApprovers.push(...verifyGroupApprovers);
await verifyApprovers({ await verifyApprovers({
projectId: project.id, projectId: project.id,
orgId: actorOrgId, orgId: actorOrgId,
@@ -76,7 +106,7 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath, secretPath,
actorAuthMethod, actorAuthMethod,
permissionService, permissionService,
userIds: approvers userIds: verifyAllApprovers
}); });
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => { const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
@@ -90,13 +120,26 @@ export const accessApprovalPolicyServiceFactory = ({
}, },
tx tx
); );
await accessApprovalPolicyApproverDAL.insertMany( if (userApprovers) {
approvers.map((userId) => ({ await accessApprovalPolicyApproverDAL.insertMany(
approverUserId: userId, userApprovers.map((userId) => ({
policyId: doc.id approverUserId: userId,
})), policyId: doc.id
tx })),
); tx
);
}
if (groupApprovers) {
await accessApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((groupId) => ({
approverGroupId: groupId,
policyId: doc.id
})),
tx
);
}
return doc; return doc;
}); });
return { ...accessApproval, environment: env, projectId: project.id }; return { ...accessApproval, environment: env, projectId: project.id };
@@ -138,7 +181,19 @@ export const accessApprovalPolicyServiceFactory = ({
approvals, approvals,
enforcementLevel enforcementLevel
}: TUpdateAccessApprovalPolicy) => { }: TUpdateAccessApprovalPolicy) => {
const groupApprovers = approvers
?.filter((approver) => approver.type === ApproverType.Group)
.map((approver) => approver.id);
const userApprovers = approvers
?.filter((approver) => approver.type === ApproverType.User)
.map((approver) => approver.id);
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId); const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
const currentAppovals = approvals || accessApprovalPolicy.approvals;
if (groupApprovers?.length === 0 && userApprovers && currentAppovals > userApprovers.length) {
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
}
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -161,7 +216,10 @@ export const accessApprovalPolicyServiceFactory = ({
}, },
tx tx
); );
if (approvers) {
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
if (userApprovers) {
await verifyApprovers({ await verifyApprovers({
projectId: accessApprovalPolicy.projectId, projectId: accessApprovalPolicy.projectId,
orgId: actorOrgId, orgId: actorOrgId,
@@ -169,18 +227,52 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath: doc.secretPath!, secretPath: doc.secretPath!,
actorAuthMethod, actorAuthMethod,
permissionService, permissionService,
userIds: approvers userIds: userApprovers
}); });
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
await accessApprovalPolicyApproverDAL.insertMany( await accessApprovalPolicyApproverDAL.insertMany(
approvers.map((userId) => ({ userApprovers.map((userId) => ({
approverUserId: userId, approverUserId: userId,
policyId: doc.id policyId: doc.id
})), })),
tx tx
); );
} }
if (groupApprovers) {
const usersPromises: Promise<
{
id: string;
email: string | null | undefined;
username: string;
firstName: string | null | undefined;
lastName: string | null | undefined;
isPartOfGroup: boolean;
}[]
>[] = [];
for (const groupId of groupApprovers) {
usersPromises.push(groupDAL.findAllGroupMembers({ orgId: actorOrgId, groupId, offset: 0 }));
}
const verifyGroupApprovers = (await Promise.all(usersPromises)).flat().map((user) => user.id);
await verifyApprovers({
projectId: accessApprovalPolicy.projectId,
orgId: actorOrgId,
envSlug: accessApprovalPolicy.environment.slug,
secretPath: doc.secretPath!,
actorAuthMethod,
permissionService,
userIds: verifyGroupApprovers
});
await accessApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((groupId) => ({
approverGroupId: groupId,
policyId: doc.id
})),
tx
);
}
return doc; return doc;
}); });
return { return {
@@ -13,11 +13,16 @@ export type TVerifyApprovers = {
orgId: string; orgId: string;
}; };
export enum ApproverType {
Group = "group",
User = "user"
}
export type TCreateAccessApprovalPolicy = { export type TCreateAccessApprovalPolicy = {
approvals: number; approvals: number;
secretPath: string; secretPath: string;
environment: string; environment: string;
approvers: string[]; approvers: { type: ApproverType; id: string }[];
projectSlug: string; projectSlug: string;
name: string; name: string;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
@@ -26,7 +31,7 @@ export type TCreateAccessApprovalPolicy = {
export type TUpdateAccessApprovalPolicy = { export type TUpdateAccessApprovalPolicy = {
policyId: string; policyId: string;
approvals?: number; approvals?: number;
approvers?: string[]; approvers?: { type: ApproverType; id: string }[];
secretPath?: string; secretPath?: string;
name?: string; name?: string;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
@@ -39,6 +39,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.AccessApprovalPolicy}.id`, `${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId` `${TableName.AccessApprovalPolicyApprover}.policyId`
) )
.leftJoin(
TableName.UserGroupMembership,
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.leftJoin(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.join<TUsers>( .join<TUsers>(
db(TableName.Users).as("requestedByUser"), db(TableName.Users).as("requestedByUser"),
@@ -59,6 +65,7 @@ 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( .select(
db.ref("projectId").withSchema(TableName.Environment), db.ref("projectId").withSchema(TableName.Environment),
@@ -142,7 +149,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
label: "reviewers" as const, label: "reviewers" as const,
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined) mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
}, },
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId } { key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId },
{
key: "approverGroupUserId",
label: "approvers" as const,
mapper: ({ approverGroupUserId }) => approverGroupUserId
}
] ]
}); });
@@ -172,17 +184,28 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
`requestedByUser.id` `requestedByUser.id`
) )
.join( .leftJoin(
TableName.AccessApprovalPolicyApprover, TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`, `${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId` `${TableName.AccessApprovalPolicyApprover}.policyId`
) )
.join<TUsers>( .leftJoin<TUsers>(
db(TableName.Users).as("accessApprovalPolicyApproverUser"), db(TableName.Users).as("accessApprovalPolicyApproverUser"),
`${TableName.AccessApprovalPolicyApprover}.approverUserId`, `${TableName.AccessApprovalPolicyApprover}.approverUserId`,
"accessApprovalPolicyApproverUser.id" "accessApprovalPolicyApproverUser.id"
) )
.leftJoin(
TableName.UserGroupMembership,
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("accessApprovalPolicyGroupApproverUser"),
`${TableName.UserGroupMembership}.userId`,
"accessApprovalPolicyGroupApproverUser.id"
)
.leftJoin( .leftJoin(
TableName.AccessApprovalRequestReviewer, TableName.AccessApprovalRequestReviewer,
@@ -200,10 +223,15 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.AccessApprovalRequest)) .select(selectAllTableCols(TableName.AccessApprovalRequest))
.select( .select(
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover), tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
tx.ref("userId").withSchema(TableName.UserGroupMembership),
tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"), tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
tx.ref("email").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
tx.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"), tx.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"),
tx.ref("username").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
tx.ref("firstName").withSchema("accessApprovalPolicyApproverUser").as("approverFirstName"), tx.ref("firstName").withSchema("accessApprovalPolicyApproverUser").as("approverFirstName"),
tx.ref("firstName").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupFirstName"),
tx.ref("lastName").withSchema("accessApprovalPolicyApproverUser").as("approverLastName"), tx.ref("lastName").withSchema("accessApprovalPolicyApproverUser").as("approverLastName"),
tx.ref("lastName").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupLastName"),
tx.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"), tx.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
tx.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"), tx.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"), tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
@@ -282,6 +310,23 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
lastName, lastName,
username username
}) })
},
{
key: "userId",
label: "approvers" as const,
mapper: ({
userId,
approverGroupEmail: email,
approverGroupUsername: username,
approverGroupLastName: lastName,
approverFirstName: firstName
}) => ({
userId,
email,
firstName,
lastName,
username
})
} }
] ]
}); });
@@ -18,6 +18,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns"; import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns";
import { TGroupDALFactory } from "../group/group-dal";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
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";
@@ -57,6 +58,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
TAccessApprovalRequestReviewerDALFactory, TAccessApprovalRequestReviewerDALFactory,
"create" | "find" | "findOne" | "transaction" "create" | "find" | "findOne" | "transaction"
>; >;
groupDAL: Pick<TGroupDALFactory, "findAllGroupMembers">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick< userDAL: Pick<
@@ -70,6 +72,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>; export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
export const accessApprovalRequestServiceFactory = ({ export const accessApprovalRequestServiceFactory = ({
groupDAL,
projectDAL, projectDAL,
projectEnvDAL, projectEnvDAL,
permissionService, permissionService,
@@ -124,13 +127,36 @@ export const accessApprovalRequestServiceFactory = ({
}); });
if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." }); if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." });
const approverIds: string[] = [];
const approverGroupIds: string[] = [];
const approvers = await accessApprovalPolicyApproverDAL.find({ const approvers = await accessApprovalPolicyApproverDAL.find({
policyId: policy.id policyId: policy.id
}); });
approvers.forEach((approver) => {
if (approver.approverUserId) {
approverIds.push(approver.approverUserId);
} else if (approver.approverGroupId) {
approverGroupIds.push(approver.approverGroupId);
}
});
const groupUsers = (
await Promise.all(
approverGroupIds.map((groupApproverId) =>
groupDAL.findAllGroupMembers({
orgId: actorOrgId,
groupId: groupApproverId
})
)
)
).flat();
approverIds.push(...groupUsers.map((user) => user.id));
const approverUsers = await userDAL.find({ const approverUsers = await userDAL.find({
$in: { $in: {
id: approvers.map((approver) => approver.approverUserId) id: [...new Set(approverIds)]
} }
}); });
@@ -1,10 +1,12 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { SecretApprovalPoliciesSchema, TableName, TSecretApprovalPolicies } from "@app/db/schemas"; import { SecretApprovalPoliciesSchema, TableName, TSecretApprovalPolicies, 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";
export type TSecretApprovalPolicyDALFactory = ReturnType<typeof secretApprovalPolicyDALFactory>; export type TSecretApprovalPolicyDALFactory = ReturnType<typeof secretApprovalPolicyDALFactory>;
export const secretApprovalPolicyDALFactory = (db: TDbClient) => { export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
@@ -20,14 +22,29 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalPolicy}.id`, `${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId` `${TableName.SecretApprovalPolicyApprover}.policyId`
) )
.leftJoin(
.leftJoin(TableName.Users, `${TableName.SecretApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`) TableName.UserGroupMembership,
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("secretApprovalPolicyApproverUser"),
`${TableName.SecretApprovalPolicyApprover}.approverUserId`,
"secretApprovalPolicyApproverUser.id"
)
.leftJoin<TUsers>(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.select( .select(
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), tx.ref("id").withSchema("secretApprovalPolicyApproverUser").as("approverUserId"),
tx.ref("email").withSchema(TableName.Users).as("approverEmail"), tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
tx.ref("firstName").withSchema(TableName.Users).as("approverFirstName"), tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"),
tx.ref("lastName").withSchema(TableName.Users).as("approverLastName") tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName")
)
.select(
tx.ref("approverGroupId").withSchema(TableName.SecretApprovalPolicyApprover),
tx.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
tx.ref("email").withSchema(TableName.Users).as("approverGroupEmail"),
tx.ref("firstName").withSchema(TableName.Users).as("approverGroupFirstName"),
tx.ref("lastName").withSchema(TableName.Users).as("approverGroupLastName")
) )
.select( .select(
tx.ref("name").withSchema(TableName.Environment).as("envName"), tx.ref("name").withSchema(TableName.Environment).as("envName"),
@@ -55,11 +72,31 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
{ {
key: "approverUserId", key: "approverUserId",
label: "userApprovers" as const, label: "userApprovers" as const,
mapper: ({ approverUserId, approverEmail, approverFirstName, approverLastName }) => ({ mapper: ({
userId: approverUserId, approverUserId: userId,
email: approverEmail, approverEmail: email,
firstName: approverFirstName, approverFirstName: firstName,
lastName: approverLastName approverLastName: lastName
}) => ({
userId,
email,
firstName,
lastName
})
},
{
key: "approverGroupUserId",
label: "userApprovers" as const,
mapper: ({
approverGroupUserId: userId,
approverGroupEmail: email,
approverGroupFirstName: firstName,
approverGroupLastName: lastName
}) => ({
userId,
email,
firstName,
lastName
}) })
} }
] ]
@@ -83,11 +120,34 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
...SecretApprovalPoliciesSchema.parse(data) ...SecretApprovalPoliciesSchema.parse(data)
}), }),
childrenMapper: [ childrenMapper: [
{
key: "approverUserId",
label: "approvers" as const,
mapper: ({ approverUserId: id }) => ({
type: ApproverType.User,
id
})
},
{
key: "approverGroupId",
label: "approvers" as const,
mapper: ({ approverGroupId: id }) => ({
type: ApproverType.Group,
id
})
},
{ {
key: "approverUserId", key: "approverUserId",
label: "userApprovers" as const, label: "userApprovers" as const,
mapper: ({ approverUserId }) => ({ mapper: ({ approverUserId: userId }) => ({
userId: approverUserId userId
})
},
{
key: "approverGroupUserId",
label: "userApprovers" as const,
mapper: ({ approverGroupUserId: userId }) => ({
userId
}) })
} }
] ]
@@ -8,6 +8,7 @@ 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 { ApproverType } from "../access-approval-policy/access-approval-policy-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal"; import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal"; import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
@@ -54,7 +55,14 @@ export const secretApprovalPolicyServiceFactory = ({
environment, environment,
enforcementLevel enforcementLevel
}: TCreateSapDTO) => { }: TCreateSapDTO) => {
if (approvals > approvers.length) const groupApprovers = approvers
?.filter((approver) => approver.type === ApproverType.Group)
.map((approver) => approver.id);
const userApprovers = approvers
?.filter((approver) => approver.type === ApproverType.User)
.map((approver) => approver.id);
if (!groupApprovers && approvals > approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
@@ -91,13 +99,22 @@ export const secretApprovalPolicyServiceFactory = ({
}, },
tx tx
); );
await secretApprovalPolicyApproverDAL.insertMany( await secretApprovalPolicyApproverDAL.insertMany(
approvers.map((approverUserId) => ({ userApprovers.map((approverUserId) => ({
approverUserId, approverUserId,
policyId: doc.id policyId: doc.id
})), })),
tx tx
); );
await secretApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((approverGroupId) => ({
approverGroupId,
policyId: doc.id
})),
tx
);
return doc; return doc;
}); });
return { ...secretApproval, environment: env, projectId }; return { ...secretApproval, environment: env, projectId };
@@ -115,6 +132,13 @@ export const secretApprovalPolicyServiceFactory = ({
secretPolicyId, secretPolicyId,
enforcementLevel enforcementLevel
}: TUpdateSapDTO) => { }: TUpdateSapDTO) => {
const groupApprovers = approvers
?.filter((approver) => approver.type === ApproverType.Group)
.map((approver) => approver.id);
const userApprovers = approvers
?.filter((approver) => approver.type === ApproverType.User)
.map((approver) => approver.id);
const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId); const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
if (!secretApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); if (!secretApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
@@ -146,16 +170,28 @@ export const secretApprovalPolicyServiceFactory = ({
}, },
tx tx
); );
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
if (approvers) { if (approvers) {
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
await secretApprovalPolicyApproverDAL.insertMany( await secretApprovalPolicyApproverDAL.insertMany(
approvers.map((approverUserId) => ({ userApprovers.map((approverUserId) => ({
approverUserId, approverUserId,
policyId: doc.id policyId: doc.id
})), })),
tx tx
); );
} }
if (groupApprovers) {
await secretApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((approverGroupId) => ({
approverGroupId,
policyId: doc.id
})),
tx
);
}
return doc; return doc;
}); });
return { return {
@@ -1,10 +1,12 @@
import { EnforcementLevel, TProjectPermission } from "@app/lib/types"; import { EnforcementLevel, TProjectPermission } from "@app/lib/types";
import { ApproverType } 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: string[]; approvers: { type: ApproverType; id: string }[];
projectId: string; projectId: string;
name: string; name: string;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
@@ -14,7 +16,7 @@ export type TUpdateSapDTO = {
secretPolicyId: string; secretPolicyId: string;
approvals?: number; approvals?: number;
secretPath?: string | null; secretPath?: string | null;
approvers: string[]; approvers: { type: ApproverType; id: string }[];
name?: string; name?: string;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -48,16 +48,26 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.committerUserId`, `${TableName.SecretApprovalRequest}.committerUserId`,
`committerUser.id` `committerUser.id`
) )
.join( .leftJoin(
TableName.SecretApprovalPolicyApprover, TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`, `${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId` `${TableName.SecretApprovalPolicyApprover}.policyId`
) )
.join<TUsers>( .leftJoin<TUsers>(
db(TableName.Users).as("secretApprovalPolicyApproverUser"), db(TableName.Users).as("secretApprovalPolicyApproverUser"),
`${TableName.SecretApprovalPolicyApprover}.approverUserId`, `${TableName.SecretApprovalPolicyApprover}.approverUserId`,
"secretApprovalPolicyApproverUser.id" "secretApprovalPolicyApproverUser.id"
) )
.leftJoin(
TableName.UserGroupMembership,
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("secretApprovalPolicyGroupApproverUser"),
`${TableName.UserGroupMembership}.userId`,
`secretApprovalPolicyGroupApproverUser.id`
)
.leftJoin( .leftJoin(
TableName.SecretApprovalRequestReviewer, TableName.SecretApprovalRequestReviewer,
`${TableName.SecretApprovalRequest}.id`, `${TableName.SecretApprovalRequest}.id`,
@@ -71,10 +81,15 @@ 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("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"), tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
tx.ref("email").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"), tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
tx.ref("username").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"), tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"),
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("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"),
@@ -152,13 +167,30 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
key: "approverUserId", key: "approverUserId",
label: "approvers" as const, label: "approvers" as const,
mapper: ({ mapper: ({
approverUserId, approverUserId: userId,
approverEmail: email, approverEmail: email,
approverUsername: username, approverUsername: username,
approverLastName: lastName, approverLastName: lastName,
approverFirstName: firstName approverFirstName: firstName
}) => ({ }) => ({
userId: approverUserId, userId,
email,
firstName,
lastName,
username
})
},
{
key: "approverGroupUserId",
label: "approvers" as const,
mapper: ({
approverGroupUserId: userId,
approverGroupEmail: email,
approverGroupUsername: username,
approverGroupLastName: lastName,
approverGroupFirstName: firstName
}) => ({
userId,
email, email,
firstName, firstName,
lastName, lastName,
@@ -236,11 +268,16 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.policyId`, `${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicy}.id` `${TableName.SecretApprovalPolicy}.id`
) )
.join( .leftJoin(
TableName.SecretApprovalPolicyApprover, TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`, `${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId` `${TableName.SecretApprovalPolicyApprover}.policyId`
) )
.leftJoin(
TableName.UserGroupMembership,
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.join<TUsers>( .join<TUsers>(
db(TableName.Users).as("committerUser"), db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`, `${TableName.SecretApprovalRequest}.committerUserId`,
@@ -269,6 +306,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
void bd void bd
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId) .where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId) .orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
.orWhere(`${TableName.UserGroupMembership}.userId`, userId)
) )
.select(selectAllTableCols(TableName.SecretApprovalRequest)) .select(selectAllTableCols(TableName.SecretApprovalRequest))
.select( .select(
@@ -289,6 +327,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
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("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"),
@@ -334,7 +373,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
{ {
key: "approverUserId", key: "approverUserId",
label: "approvers" as const, label: "approvers" as const,
mapper: ({ approverUserId }) => approverUserId mapper: ({ approverUserId }) => ({ userId: approverUserId })
}, },
{ {
key: "commitId", key: "commitId",
@@ -344,6 +383,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
id, id,
secretId secretId
}) })
},
{
key: "approverGroupUserId",
label: "approvers" as const,
mapper: ({ approverGroupUserId }) => ({ userId: approverGroupUserId })
} }
] ]
}); });
@@ -371,11 +415,16 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.policyId`, `${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicy}.id` `${TableName.SecretApprovalPolicy}.id`
) )
.join( .leftJoin(
TableName.SecretApprovalPolicyApprover, TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`, `${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId` `${TableName.SecretApprovalPolicyApprover}.policyId`
) )
.leftJoin(
TableName.UserGroupMembership,
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.join<TUsers>( .join<TUsers>(
db(TableName.Users).as("committerUser"), db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`, `${TableName.SecretApprovalRequest}.committerUserId`,
@@ -404,6 +453,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
void bd void bd
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId) .where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId) .orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
.orWhere(`${TableName.UserGroupMembership}.userId`, userId)
) )
.select(selectAllTableCols(TableName.SecretApprovalRequest)) .select(selectAllTableCols(TableName.SecretApprovalRequest))
.select( .select(
@@ -424,6 +474,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
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("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"),
@@ -469,7 +520,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
{ {
key: "approverUserId", key: "approverUserId",
label: "approvers" as const, label: "approvers" as const,
mapper: ({ approverUserId }) => approverUserId mapper: ({ approverUserId }) => ({ userId: approverUserId })
}, },
{ {
key: "commitId", key: "commitId",
@@ -479,6 +530,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
id, id,
secretId secretId
}) })
},
{
key: "approverGroupUserId",
label: "approvers" as const,
mapper: ({ approverGroupUserId }) => ({
userId: approverGroupUserId
})
} }
] ]
}); });
@@ -447,8 +447,8 @@ export const secretApprovalRequestServiceFactory = ({
); );
const hasMinApproval = const hasMinApproval =
secretApprovalRequest.policy.approvals <= secretApprovalRequest.policy.approvals <=
secretApprovalRequest.policy.approvers.filter( secretApprovalRequest.policy.approvers.filter(({ userId: approverId }) =>
({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED approverId ? reviewers[approverId] === ApprovalStatus.APPROVED : false
).length; ).length;
const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft; const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft;
@@ -805,7 +805,7 @@ export const secretApprovalRequestServiceFactory = ({
const requestedByUser = await userDAL.findOne({ id: actorId }); const requestedByUser = await userDAL.findOne({ id: actorId });
const approverUsers = await userDAL.find({ const approverUsers = await userDAL.find({
$in: { $in: {
id: policy.approvers.map((approver: { userId: string }) => approver.userId) id: policy.approvers.map((approver: { userId: string | null | undefined }) => approver.userId!)
} }
}); });
+3 -1
View File
@@ -923,6 +923,7 @@ export const registerRoutes = async (
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({ const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
accessApprovalPolicyDAL, accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
groupDAL,
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
projectMembershipDAL, projectMembershipDAL,
@@ -942,7 +943,8 @@ export const registerRoutes = async (
smtpService, smtpService,
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
projectSlackConfigDAL, projectSlackConfigDAL,
kmsService kmsService,
groupDAL
}); });
const secretReplicationService = secretReplicationServiceFactory({ const secretReplicationService = secretReplicationServiceFactory({
+13 -4
View File
@@ -11,14 +11,23 @@ export type TAccessApprovalPolicy = {
workspace: string; workspace: string;
environment: WorkspaceEnv; environment: WorkspaceEnv;
projectId: string; projectId: string;
approvers: string[];
policyType: PolicyType; policyType: PolicyType;
approversRequired: boolean; approversRequired: boolean;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
updatedAt: Date; updatedAt: Date;
userApprovers?: { userId: string }[]; approvers?: Approver[];
}; };
export enum ApproverType{
User = "user",
Group = "group"
}
export type Approver ={
id: string;
type: ApproverType;
}
export type TAccessApprovalRequest = { export type TAccessApprovalRequest = {
id: string; id: string;
policyId: string; policyId: string;
@@ -130,7 +139,7 @@ export type TCreateAccessPolicyDTO = {
projectSlug: string; projectSlug: string;
name?: string; name?: string;
environment: string; environment: string;
approvers?: string[]; approvers?: Approver[];
approvals?: number; approvals?: number;
secretPath?: string; secretPath?: string;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
@@ -139,7 +148,7 @@ export type TCreateAccessPolicyDTO = {
export type TUpdateAccessPolicyDTO = { export type TUpdateAccessPolicyDTO = {
id: string; id: string;
name?: string; name?: string;
approvers?: string[]; approvers?: Approver[];
secretPath?: string; secretPath?: string;
environment?: string; environment?: string;
approvals?: number; approvals?: number;
+13 -3
View File
@@ -9,11 +9,21 @@ export type TSecretApprovalPolicy = {
environment: WorkspaceEnv; environment: WorkspaceEnv;
secretPath?: string; secretPath?: string;
approvals: number; approvals: number;
userApprovers: { userId: string }[]; approvers: Approver[];
updatedAt: Date; updatedAt: Date;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
}; };
export enum ApproverType{
User = "user",
Group = "group"
}
export type Approver ={
id: string;
type: ApproverType;
}
export type TGetSecretApprovalPoliciesDTO = { export type TGetSecretApprovalPoliciesDTO = {
workspaceId: string; workspaceId: string;
}; };
@@ -29,7 +39,7 @@ export type TCreateSecretPolicyDTO = {
name?: string; name?: string;
environment: string; environment: string;
secretPath?: string | null; secretPath?: string | null;
approvers?: string[]; approvers?: Approver[];
approvals?: number; approvals?: number;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
}; };
@@ -37,7 +47,7 @@ export type TCreateSecretPolicyDTO = {
export type TUpdateSecretPolicyDTO = { export type TUpdateSecretPolicyDTO = {
id: string; id: string;
name?: string; name?: string;
approvers?: string[]; approvers?: Approver[];
secretPath?: string | null; secretPath?: string | null;
approvals?: number; approvals?: number;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
@@ -41,7 +41,8 @@ import {
useDeleteAccessApprovalPolicy, useDeleteAccessApprovalPolicy,
useDeleteSecretApprovalPolicy, useDeleteSecretApprovalPolicy,
useGetSecretApprovalPolicies, useGetSecretApprovalPolicies,
useGetWorkspaceUsers useGetWorkspaceUsers,
useListWorkspaceGroups
} from "@app/hooks/api"; } from "@app/hooks/api";
import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries"; import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries";
import { PolicyType } from "@app/hooks/api/policies/enums"; import { PolicyType } from "@app/hooks/api/policies/enums";
@@ -102,6 +103,8 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data: members } = useGetWorkspaceUsers(workspaceId, true); const { data: members } = useGetWorkspaceUsers(workspaceId, true);
const { data: groups } = useListWorkspaceGroups(currentWorkspace?.slug || "");
const { policies, isLoading: isPoliciesLoading } = useApprovalPolicies( const { policies, isLoading: isPoliciesLoading } = useApprovalPolicies(
permission, permission,
currentWorkspace currentWorkspace
@@ -186,6 +189,7 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
<Th>Environment</Th> <Th>Environment</Th>
<Th>Secret Path</Th> <Th>Secret Path</Th>
<Th>Eligible Approvers</Th> <Th>Eligible Approvers</Th>
<Th>Eligible Group Approvers</Th>
<Th>Approval Required</Th> <Th>Approval Required</Th>
<Th> <Th>
<DropdownMenu> <DropdownMenu>
@@ -257,6 +261,7 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
workspaceId={workspaceId} workspaceId={workspaceId}
key={policy.id} key={policy.id}
members={members} members={members}
groups={groups}
onEdit={() => handlePopUpOpen("policyForm", policy)} onEdit={() => handlePopUpOpen("policyForm", policy)}
onDelete={() => handlePopUpOpen("deletePolicy", policy)} onDelete={() => handlePopUpOpen("deletePolicy", policy)}
/> />
@@ -22,12 +22,12 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { policyDetails } from "@app/helpers/policies"; import { policyDetails } from "@app/helpers/policies";
import { useCreateSecretApprovalPolicy, useUpdateSecretApprovalPolicy } from "@app/hooks/api"; import { useCreateSecretApprovalPolicy, useListWorkspaceGroups, useUpdateSecretApprovalPolicy } from "@app/hooks/api";
import { import {
useCreateAccessApprovalPolicy, useCreateAccessApprovalPolicy,
useUpdateAccessApprovalPolicy useUpdateAccessApprovalPolicy
} from "@app/hooks/api/accessApproval"; } from "@app/hooks/api/accessApproval";
import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types"; import { ApproverType, 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";
@@ -45,13 +45,13 @@ const formSchema = z
name: z.string().optional(), name: z.string().optional(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
approvals: z.number().min(1), approvals: z.number().min(1),
approvers: z.string().array().min(1), approvers: z.object({type: z.nativeEnum(ApproverType), id: z.string()}).array().min(1).default([]),
policyType: z.nativeEnum(PolicyType), policyType: z.nativeEnum(PolicyType),
enforcementLevel: z.nativeEnum(EnforcementLevel) enforcementLevel: z.nativeEnum(EnforcementLevel)
}) })
.refine((data) => data.approvals <= data.approvers.length, { .refine((data) => data.approvers, {
path: ["approvals"], path: ["approvers"],
message: "The number of approvals should be lower than the number of approvers." message: "At least one approver should be provided."
}); });
type TFormSchema = z.infer<typeof formSchema>; type TFormSchema = z.infer<typeof formSchema>;
@@ -75,11 +75,13 @@ export const AccessPolicyForm = ({
? { ? {
...editValues, ...editValues,
environment: editValues.environment.slug, environment: editValues.environment.slug,
approvers: editValues?.userApprovers?.map((user) => user.userId) || editValues?.approvers approvers: editValues?.approvers || [],
approvals: editValues?.approvals
} }
: undefined : undefined
}); });
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data: groups } = useListWorkspaceGroups(projectSlug);
const environments = currentWorkspace?.environments || []; const environments = currentWorkspace?.environments || [];
const isEditMode = Boolean(editValues); const isEditMode = Boolean(editValues);
@@ -266,8 +268,7 @@ export const AccessPolicyForm = ({
name="approvers" name="approvers"
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl <FormControl
label="Required Approvers" label="Required User Approvers"
isRequired
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
> >
@@ -288,15 +289,15 @@ export const AccessPolicyForm = ({
</DropdownMenuLabel> </DropdownMenuLabel>
{members.map(({ user }) => { {members.map(({ user }) => {
const { id: userId } = user; const { id: userId } = user;
const isChecked = value?.includes(userId); const isChecked = value?.filter((el: {id: string, type: ApproverType}) => el.id === userId && el.type === ApproverType.User).length > 0;
return ( return (
<DropdownMenuItem <DropdownMenuItem
onClick={(evt) => { onClick={(evt) => {
evt.preventDefault(); evt.preventDefault();
onChange( onChange(
isChecked isChecked
? value?.filter((el: string) => el !== userId) ? value?.filter((el: {id: string, type: ApproverType}) => el.id !== userId && el.type !== ApproverType.User)
: [...(value || []), userId] : [...(value || []), {id:userId, type: ApproverType.User}]
); );
}} }}
key={`create-policy-members-${userId}`} key={`create-policy-members-${userId}`}
@@ -312,6 +313,57 @@ export const AccessPolicyForm = ({
</FormControl> </FormControl>
)} )}
/> />
<Controller
control={control}
name="approvers"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Required Group Approvers"
isError={Boolean(error)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Input
isReadOnly
value={value?.length ? `${value.length} selected` : "None"}
className="text-left"
/>
</DropdownMenuTrigger>
<DropdownMenuContent
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
align="start"
>
<DropdownMenuLabel>
Select groups that are allowed to approve requests
</DropdownMenuLabel>
{groups && groups.map(({ group }) => {
const { id } = group;
const isChecked = value?.includes({id, type: ApproverType.Group});
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
onChange(
isChecked
? value?.filter((el: {id: string, type: ApproverType}) => el.id !== id && el.type !== ApproverType.Group)
: [...(value || []), {id, type: ApproverType.Group}]
);
}}
key={`create-policy-members-${id}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
{group.name}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
<Controller <Controller
control={control} control={control}
name="approvals" name="approvals"
@@ -18,6 +18,8 @@ import { Badge } from "@app/components/v2/Badge";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { policyDetails } from "@app/helpers/policies"; import { policyDetails } from "@app/helpers/policies";
import { useUpdateAccessApprovalPolicy, useUpdateSecretApprovalPolicy } from "@app/hooks/api"; import { useUpdateAccessApprovalPolicy, useUpdateSecretApprovalPolicy } from "@app/hooks/api";
import { Approver, ApproverType } from "@app/hooks/api/accessApproval/types";
import { TGroupMembership } from "@app/hooks/api/groups/types";
import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums"; import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums";
import { WorkspaceEnv } from "@app/hooks/api/types"; import { WorkspaceEnv } from "@app/hooks/api/types";
import { TWorkspaceUser } from "@app/hooks/api/users/types"; import { TWorkspaceUser } from "@app/hooks/api/users/types";
@@ -29,8 +31,7 @@ interface IPolicy {
projectId?: string; projectId?: string;
secretPath?: string; secretPath?: string;
approvals: number; approvals: number;
approvers?: string[]; approvers?: Approver[];
userApprovers?: { userId: string }[];
updatedAt: Date; updatedAt: Date;
policyType: PolicyType; policyType: PolicyType;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
@@ -39,6 +40,7 @@ interface IPolicy {
type Props = { type Props = {
policy: IPolicy; policy: IPolicy;
members?: TWorkspaceUser[]; members?: TWorkspaceUser[];
groups?: TGroupMembership[];
projectSlug: string; projectSlug: string;
workspaceId: string; workspaceId: string;
onEdit: () => void; onEdit: () => void;
@@ -48,12 +50,14 @@ type Props = {
export const ApprovalPolicyRow = ({ export const ApprovalPolicyRow = ({
policy, policy,
members = [], members = [],
groups = [],
projectSlug, projectSlug,
workspaceId, workspaceId,
onEdit, onEdit,
onDelete onDelete
}: Props) => { }: Props) => {
const [selectedApprovers, setSelectedApprovers] = useState<string[]>(policy.userApprovers?.map(({ userId }) => userId) || policy.approvers || []); const [selectedApprovers, setSelectedApprovers] = useState<Approver[]>(policy.approvers?.filter((approver) => approver.type === ApproverType.User) || []);
const [selectedGroupApprovers, setSelectedGroupApprovers] = useState<Approver[]>(policy.approvers?.filter((approver) => approver.type === ApproverType.Group) || []);
const { mutate: updateAccessApprovalPolicy, isLoading: isAccessApprovalPolicyLoading } = useUpdateAccessApprovalPolicy(); const { mutate: updateAccessApprovalPolicy, isLoading: isAccessApprovalPolicyLoading } = useUpdateAccessApprovalPolicy();
const { mutate: updateSecretApprovalPolicy, isLoading: isSecretApprovalPolicyLoading } = useUpdateSecretApprovalPolicy(); const { mutate: updateSecretApprovalPolicy, isLoading: isSecretApprovalPolicyLoading } = useUpdateSecretApprovalPolicy();
const isLoading = isAccessApprovalPolicyLoading || isSecretApprovalPolicyLoading; const isLoading = isAccessApprovalPolicyLoading || isSecretApprovalPolicyLoading;
@@ -74,28 +78,33 @@ export const ApprovalPolicyRow = ({
{ {
projectSlug, projectSlug,
id: policy.id, id: policy.id,
approvers: selectedApprovers approvers: selectedApprovers.concat(selectedGroupApprovers),
}, },
{ onSettled: () => {} } {
onError: () => {
setSelectedApprovers(policy?.approvers?.filter((approver) => approver.type === ApproverType.User) || []);
}
}
); );
} else { } else {
updateSecretApprovalPolicy( updateSecretApprovalPolicy(
{ {
workspaceId, workspaceId,
id: policy.id, id: policy.id,
approvers: selectedApprovers approvers: selectedApprovers.concat(selectedGroupApprovers),
}, },
{ onSettled: () => {} } {
onError: () => {
setSelectedApprovers(policy?.approvers?.filter((approver) => approver.type === ApproverType.User) || []);
}
}
); );
} }
} else { } else {
setSelectedApprovers(policy.policyType === PolicyType.ChangePolicy setSelectedApprovers(policy?.approvers?.filter((approver) => approver.type === ApproverType.User) || []);
? policy?.userApprovers?.map(({ userId }) => userId) || []
: policy?.approvers || []
);
} }
}} }}
> >
<DropdownMenuTrigger <DropdownMenuTrigger
asChild asChild
disabled={ disabled={
@@ -116,15 +125,15 @@ export const ApprovalPolicyRow = ({
<DropdownMenuLabel> <DropdownMenuLabel>
Select members that are allowed to approve changes Select members that are allowed to approve changes
</DropdownMenuLabel> </DropdownMenuLabel>
{members?.map(({ id, user }) => { {members?.map(({ user }) => {
const userId = policy.policyType === PolicyType.ChangePolicy ? user.id : id; const userId = user.id;
const isChecked = selectedApprovers.includes(userId); const isChecked = selectedApprovers?.filter((el: { id: string, type: ApproverType }) => el.id === userId && el.type === ApproverType.User).length > 0;
return ( return (
<DropdownMenuItem <DropdownMenuItem
onClick={(evt) => { onClick={(evt) => {
evt.preventDefault(); evt.preventDefault();
setSelectedApprovers((state) => setSelectedApprovers((state) =>
isChecked ? state.filter((el) => el !== userId) : [...state, userId] isChecked ? state.filter((el) => el.id !== userId || el.type !== ApproverType.User) : [...state, { id: userId, type: ApproverType.User }]
); );
}} }}
key={`create-policy-members-${userId}`} key={`create-policy-members-${userId}`}
@@ -138,6 +147,80 @@ export const ApprovalPolicyRow = ({
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</Td> </Td>
<Td>
<DropdownMenu
onOpenChange={(isOpen) => {
if (!isOpen) {
if (policy.policyType === PolicyType.AccessPolicy) {
updateAccessApprovalPolicy(
{
projectSlug,
id: policy.id,
approvers: selectedApprovers.concat(selectedGroupApprovers),
},
{
onError: () => {
setSelectedGroupApprovers(policy?.approvers?.filter((approver) => approver.type === ApproverType.Group) || []);
}
},
);
} else {
updateSecretApprovalPolicy(
{
workspaceId,
id: policy.id,
approvers: selectedApprovers.concat(selectedGroupApprovers),
},
{
onError: () => {
setSelectedGroupApprovers(policy?.approvers?.filter((approver) => approver.type === ApproverType.Group) || []);
}
}
);
}
} else {
setSelectedGroupApprovers(policy?.approvers?.filter((approver) => approver.type === ApproverType.Group) || []);
}
}}
>
<DropdownMenuTrigger asChild>
<Input
isReadOnly
value={selectedGroupApprovers?.length ? `${selectedGroupApprovers.length} selected` : "None"}
className="text-left"
/>
</DropdownMenuTrigger>
<DropdownMenuContent
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
align="start"
>
<DropdownMenuLabel>
Select groups that are allowed to approve requests
</DropdownMenuLabel>
{groups && groups.map(({ group }) => {
const { id } = group;
const isChecked = selectedGroupApprovers?.filter((el: { id: string, type: ApproverType }) => el.id === id && el.type === ApproverType.Group).length > 0;
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
setSelectedGroupApprovers(
isChecked
? selectedGroupApprovers?.filter((el) => el.id !== id || el.type !== ApproverType.Group)
: [...(selectedGroupApprovers || []), { id, type: ApproverType.Group }]
);
}}
key={`create-policy-groups-${id}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
{group.name}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</Td>
<Td>{policy.approvals}</Td> <Td>{policy.approvals}</Td>
<Td> <Td>
<Badge className={policyDetails[policy.policyType].className}> <Badge className={policyDetails[policy.policyType].className}>