From 6733349af03b6f4f9885b71cb72405f49d092f7f Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 25 Sep 2024 00:07:11 +0800 Subject: [PATCH] misc: updated secret approval policy api to support TF usecase --- .../v1/secret-approval-policy-router.ts | 48 +++++++- .../secret-approval-policy-dal.ts | 27 ++++- .../secret-approval-policy-service.ts | 103 +++++++++++++++++- .../secret-approval-policy-types.ts | 6 +- 4 files changed, 170 insertions(+), 14 deletions(-) diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v1/secret-approval-policy-router.ts index 80b77aabf..49a080e84 100644 --- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-policy-router.ts @@ -28,7 +28,10 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi .default("/") .transform((val) => (val ? removeTrailingSlash(val) : val)), approvers: z - .object({ type: z.nativeEnum(ApproverType), id: z.string() }) + .discriminatedUnion("type", [ + 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() }) + ]) .array() .min(1, { message: "At least one approver should be provided" }), approvals: z.number().min(1).default(1), @@ -69,7 +72,10 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi body: z.object({ name: z.string().optional(), approvers: z - .object({ type: z.nativeEnum(ApproverType), id: z.string() }) + .discriminatedUnion("type", [ + 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() }) + ]) .array() .min(1, { message: "At least one approver should be provided" }), approvals: z.number().min(1).default(1), @@ -168,6 +174,44 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi } }); + server.route({ + url: "/:sapId", + method: "GET", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + sapId: z.string() + }), + response: { + 200: z.object({ + approval: sapPubSchema.extend({ + approvers: z + .object({ + id: z.string().nullable().optional(), + type: z.nativeEnum(ApproverType), + name: z.string().nullable().optional() + }) + .array() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const approval = await server.services.secretApprovalPolicy.getSecretApprovalPolicyById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.params + }); + + return { approval }; + } + }); + server.route({ url: "/board", method: "GET", diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts index 651e19b64..e3526b0e3 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts @@ -12,10 +12,21 @@ export type TSecretApprovalPolicyDALFactory = ReturnType { const secretApprovalPolicyOrm = ormify(db, TableName.SecretApprovalPolicy); - const secretApprovalPolicyFindQuery = (tx: Knex, filter: TFindFilter) => + const secretApprovalPolicyFindQuery = ( + tx: Knex, + filter: TFindFilter, + customFilter?: { + sapId?: string; + } + ) => tx(TableName.SecretApprovalPolicy) // eslint-disable-next-line .where(buildFindFilter(filter)) + .where((qb) => { + if (customFilter?.sapId) { + void qb.where(`${TableName.SecretApprovalPolicy}.id`, "=", customFilter.sapId); + } + }) .join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`) .leftJoin( TableName.SecretApprovalPolicyApprover, @@ -37,6 +48,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => { tx.ref("id").withSchema("secretApprovalPolicyApproverUser").as("approverUserId"), tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"), tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"), + tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"), tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName") ) .select( @@ -108,9 +120,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => { } }; - const find = async (filter: TFindFilter, tx?: Knex) => { + const find = async ( + filter: TFindFilter, + customFilter?: { + sapId?: string; + }, + tx?: Knex + ) => { try { - const docs = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), filter); + const docs = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), filter, customFilter); const formatedDoc = sqlNestRelationships({ data: docs, key: "id", @@ -123,8 +141,9 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => { { key: "approverUserId", label: "approvers" as const, - mapper: ({ approverUserId: id }) => ({ + mapper: ({ approverUserId: id, approverUsername }) => ({ type: ApproverType.User, + name: approverUsername, id }) }, diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index c1f7cce31..5af05a71f 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -3,10 +3,11 @@ import picomatch from "picomatch"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { BadRequestError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; import { containsGlobPatterns } from "@app/lib/picomatch"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; +import { TUserDALFactory } from "@app/services/user/user-dal"; import { ApproverType } from "../access-approval-policy/access-approval-policy-types"; import { TLicenseServiceFactory } from "../license/license-service"; @@ -16,6 +17,7 @@ import { TCreateSapDTO, TDeleteSapDTO, TGetBoardSapDTO, + TGetSapByIdDTO, TListSapDTO, TUpdateSapDTO } from "./secret-approval-policy-types"; @@ -29,6 +31,7 @@ type TSecretApprovalPolicyServiceFactoryDep = { permissionService: Pick; secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory; projectEnvDAL: Pick; + userDAL: Pick; secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory; licenseService: Pick; }; @@ -40,6 +43,7 @@ export const secretApprovalPolicyServiceFactory = ({ permissionService, secretApprovalPolicyApproverDAL, projectEnvDAL, + userDAL, licenseService }: TSecretApprovalPolicyServiceFactoryDep) => { const createSecretApprovalPolicy = async ({ @@ -60,7 +64,12 @@ export const secretApprovalPolicyServiceFactory = ({ .map((approver) => approver.id); const userApprovers = approvers ?.filter((approver) => approver.type === ApproverType.User) - .map((approver) => approver.id); + .map((approver) => approver.id) + .filter(Boolean) as string[]; + + const userApproverNames = approvers + .map((approver) => (approver.type === ApproverType.User ? approver.name : undefined)) + .filter(Boolean) as string[]; if (!groupApprovers && approvals > approvers.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); @@ -100,8 +109,31 @@ export const secretApprovalPolicyServiceFactory = ({ tx ); + let userApproverIds = userApprovers; + if (userApproverNames.length) { + const approverUsers = await userDAL.find( + { + $in: { + username: userApproverNames + } + }, + { tx } + ); + + const approverNamesFromDb = approverUsers.map((user) => user.username); + const invalidUsernames = userApproverNames?.filter((username) => !approverNamesFromDb.includes(username)); + + if (invalidUsernames?.length) { + throw new BadRequestError({ + message: `Invalid approver user: ${invalidUsernames.join(", ")}` + }); + } + + userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id)); + } + await secretApprovalPolicyApproverDAL.insertMany( - userApprovers.map((approverUserId) => ({ + userApproverIds.map((approverUserId) => ({ approverUserId, policyId: doc.id })), @@ -117,6 +149,7 @@ export const secretApprovalPolicyServiceFactory = ({ ); return doc; }); + return { ...secretApproval, environment: env, projectId }; }; @@ -137,7 +170,12 @@ export const secretApprovalPolicyServiceFactory = ({ .map((approver) => approver.id); const userApprovers = approvers ?.filter((approver) => approver.type === ApproverType.User) - .map((approver) => approver.id); + .map((approver) => approver.id) + .filter(Boolean) as string[]; + + const userApproverNames = approvers + .map((approver) => (approver.type === ApproverType.User ? approver.name : undefined)) + .filter(Boolean) as string[]; const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId); if (!secretApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); @@ -174,8 +212,31 @@ export const secretApprovalPolicyServiceFactory = ({ await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx); if (approvers) { + let userApproverIds = userApprovers; + if (userApproverNames) { + const approverUsers = await userDAL.find( + { + $in: { + username: userApproverNames + } + }, + { tx } + ); + + const approverNamesFromDb = approverUsers.map((user) => user.username); + const invalidUsernames = userApproverNames?.filter((username) => !approverNamesFromDb.includes(username)); + + if (invalidUsernames?.length) { + throw new BadRequestError({ + message: `Invalid approver user: ${invalidUsernames.join(", ")}` + }); + } + + userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id)); + } + await secretApprovalPolicyApproverDAL.insertMany( - userApprovers.map((approverUserId) => ({ + userApproverIds.map((approverUserId) => ({ approverUserId, policyId: doc.id })), @@ -192,6 +253,7 @@ export const secretApprovalPolicyServiceFactory = ({ tx ); } + return doc; }); return { @@ -296,12 +358,41 @@ export const secretApprovalPolicyServiceFactory = ({ return getSecretApprovalPolicy(projectId, environment, secretPath); }; + const getSecretApprovalPolicyById = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + sapId + }: TGetSapByIdDTO) => { + const [sapPolicy] = await secretApprovalPolicyDAL.find({}, { sapId }); + + if (!sapPolicy) { + throw new NotFoundError({ + message: "Cannot find secret approval policy" + }); + } + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + sapPolicy.projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + + return sapPolicy; + }; + return { createSecretApprovalPolicy, updateSecretApprovalPolicy, deleteSecretApprovalPolicy, getSecretApprovalPolicy, getSecretApprovalPolicyByProjectId, - getSecretApprovalPolicyOfFolder + getSecretApprovalPolicyOfFolder, + getSecretApprovalPolicyById }; }; diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts index 78b77c152..863f1c926 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts @@ -6,7 +6,7 @@ export type TCreateSapDTO = { approvals: number; secretPath?: string | null; environment: string; - approvers: { type: ApproverType; id: string }[]; + approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[]; projectId: string; name: string; enforcementLevel: EnforcementLevel; @@ -16,7 +16,7 @@ export type TUpdateSapDTO = { secretPolicyId: string; approvals?: number; secretPath?: string | null; - approvers: { type: ApproverType; id: string }[]; + approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[]; name?: string; enforcementLevel?: EnforcementLevel; } & Omit; @@ -27,6 +27,8 @@ export type TDeleteSapDTO = { export type TListSapDTO = TProjectPermission; +export type TGetSapByIdDTO = Omit & { sapId: string }; + export type TGetBoardSapDTO = { projectId: string; environment: string;