diff --git a/backend/src/controllers/v1/index.ts b/backend/src/controllers/v1/index.ts index a9bde9971..9dcbb12c2 100644 --- a/backend/src/controllers/v1/index.ts +++ b/backend/src/controllers/v1/index.ts @@ -16,6 +16,7 @@ import * as workspaceController from "./workspaceController"; import * as secretScanningController from "./secretScanningController"; import * as webhookController from "./webhookController"; import * as secretImpsController from "./secretImpsController"; +import * as secretApprovalController from "./secretApprovalController"; export { authController, @@ -35,5 +36,6 @@ export { workspaceController, secretScanningController, webhookController, - secretImpsController + secretImpsController, + secretApprovalController }; diff --git a/backend/src/controllers/v1/secretApprovalController.ts b/backend/src/controllers/v1/secretApprovalController.ts new file mode 100644 index 000000000..8441a93b0 --- /dev/null +++ b/backend/src/controllers/v1/secretApprovalController.ts @@ -0,0 +1,109 @@ +import { ForbiddenError } from "@casl/ability"; +import { Request, Response } from "express"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; +import { validateRequest } from "../../helpers/validation"; +import { SecretApproval } from "../../models/secretApproval"; +import { BadRequestError } from "../../utils/errors"; +import * as reqValidator from "../../validation/secretApproval"; + +const ERR_SECRET_APPROVAL_NOT_FOUND = BadRequestError({ message: "secret approval not found" }); + +export const createSecretApprovalRule = async (req: Request, res: Response) => { + const { + body: { approvals, secretPath, approvers, environment, workspaceId } + } = await validateRequest(reqValidator.CreateSecretApprovalRule, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.SecretApproval + ); + + const secretApproval = new SecretApproval({ + workspace: workspaceId, + secretPath, + environment, + approvals, + approvers + }); + await secretApproval.save(); + + return res.send({ + approval: secretApproval + }); +}; + +export const updateSecretApprovalRule = async (req: Request, res: Response) => { + const { + body: { approvals, approvers, secretPath }, + params: { id } + } = await validateRequest(reqValidator.UpdateSecretApprovalRule, req); + + const secretApproval = await SecretApproval.findById(id); + if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND; + + const { permission } = await getUserProjectPermissions( + req.user._id, + secretApproval.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.SecretApproval + ); + + const updatedDoc = await SecretApproval.findByIdAndUpdate(id, { + approvals, + approvers, + $set: secretPath === "-" ? undefined : { secretPath } + }); + + return res.send({ + approval: updatedDoc + }); +}; + +export const deleteSecretApprovalRule = async (req: Request, res: Response) => { + const { + params: { id } + } = await validateRequest(reqValidator.DeleteSecretApprovalRule, req); + + const secretApproval = await SecretApproval.findById(id); + if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND; + + const { permission } = await getUserProjectPermissions( + req.user._id, + secretApproval.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.SecretApproval + ); + + const deletedDoc = await SecretApproval.findByIdAndDelete(id); + + return res.send({ + approval: deletedDoc + }); +}; + +export const getSecretApprovalRules = async (req: Request, res: Response) => { + const { + query: { workspaceId } + } = await validateRequest(reqValidator.GetSecretApprovalRuleList, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretApproval + ); + + const doc = await SecretApproval.find({ workspace: workspaceId }); + + return res.send({ + approvals: doc + }); +}; diff --git a/backend/src/ee/services/ProjectRoleService.ts b/backend/src/ee/services/ProjectRoleService.ts index 51f4a26f1..867edb7e5 100644 --- a/backend/src/ee/services/ProjectRoleService.ts +++ b/backend/src/ee/services/ProjectRoleService.ts @@ -49,7 +49,8 @@ export enum ProjectPermissionSub { IpAllowList = "ip-allowlist", Workspace = "workspace", Secrets = "secrets", - SecretRollback = "secret-rollback" + SecretRollback = "secret-rollback", + SecretApproval = "secret-approval" } type SubjectFields = { @@ -72,6 +73,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] | [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] + | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] @@ -85,6 +87,11 @@ const buildAdminPermission = () => { can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); @@ -154,6 +161,8 @@ const buildMemberPermission = () => { can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); @@ -203,6 +212,7 @@ const buildViewerPermission = () => { const { can, build } = new AbilityBuilder>(createMongoAbility); can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); diff --git a/backend/src/index.ts b/backend/src/index.ts index aa4440a55..c3aed4b7b 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -38,6 +38,7 @@ import { membership as v1MembershipRouter, organization as v1OrganizationRouter, password as v1PasswordRouter, + secretApproval as v1SecretApproval, secretImps as v1SecretImpsRouter, secret as v1SecretRouter, secretsFolder as v1SecretsFolder, @@ -176,6 +177,7 @@ const main = async () => { app.use("/api/v1/webhooks", v1WebhooksRouter); app.use("/api/v1/secret-imports", v1SecretImpsRouter); app.use("/api/v1/roles", v1RoleRouter); + app.use("/api/v1/secret-approvals", v1SecretApproval); // v2 routes (improvements) app.use("/api/v2/signup", v2SignupRouter); diff --git a/backend/src/routes/v1/index.ts b/backend/src/routes/v1/index.ts index 46e74d58c..7c0f94972 100644 --- a/backend/src/routes/v1/index.ts +++ b/backend/src/routes/v1/index.ts @@ -17,6 +17,7 @@ import integrationAuth from "./integrationAuth"; import secretsFolder from "./secretsFolder"; import webhooks from "./webhook"; import secretImps from "./secretImps"; +import secretApproval from "./secretApproval"; export { signup, @@ -37,5 +38,6 @@ export { integrationAuth, secretsFolder, webhooks, - secretImps + secretImps, + secretApproval }; diff --git a/backend/src/routes/v1/secretApproval.ts b/backend/src/routes/v1/secretApproval.ts new file mode 100644 index 000000000..213bb3cc3 --- /dev/null +++ b/backend/src/routes/v1/secretApproval.ts @@ -0,0 +1,39 @@ +import express from "express"; +const router = express.Router(); +import { requireAuth } from "../../middleware"; +import { secretApprovalController } from "../../controllers/v1"; +import { AuthMode } from "../../variables"; + +router.get( + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretApprovalController.getSecretApprovalRules +); + +router.post( + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretApprovalController.createSecretApprovalRule +); + +router.patch( + "/:id", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretApprovalController.updateSecretApprovalRule +); + +router.delete( + "/:id", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretApprovalController.deleteSecretApprovalRule +); + +export default router; diff --git a/backend/src/validation/index.ts b/backend/src/validation/index.ts index 409b563c6..85899a535 100644 --- a/backend/src/validation/index.ts +++ b/backend/src/validation/index.ts @@ -1,3 +1,4 @@ +export * from "./secretApproval"; export * from "./user"; export * from "./workspace"; export * from "./bot"; diff --git a/backend/src/validation/secretApproval.ts b/backend/src/validation/secretApproval.ts new file mode 100644 index 000000000..a25da81b2 --- /dev/null +++ b/backend/src/validation/secretApproval.ts @@ -0,0 +1,34 @@ +import { z } from "zod"; + +export const GetSecretApprovalRuleList = z.object({ + query: z.object({ + workspaceId: z.string() + }) +}); + +export const CreateSecretApprovalRule = z.object({ + body: z.object({ + workspaceId: z.string(), + environment: z.string(), + secretPath: z.string().optional(), + approvers: z.string().array().optional(), + approvals: z.number().min(1).default(1) + }) +}); + +export const UpdateSecretApprovalRule = z.object({ + params: z.object({ + id: z.string() + }), + body: z.object({ + approvers: z.string().array().optional(), + approvals: z.number().min(1).optional(), + secretPath: z.string().optional() + }) +}); + +export const DeleteSecretApprovalRule = z.object({ + params: z.object({ + id: z.string() + }) +});