From 9cda85f03ed44c4f29a677782f59df6f9eb05467 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Mon, 4 Sep 2023 15:05:54 +0530 Subject: [PATCH 1/4] checkpoint --- backend/src/models/secretApproval.ts | 44 ++++++ backend/src/models/secretApprovalRequest.ts | 128 ++++++++---------- frontend/public/locales/en/translations.json | 5 + frontend/src/layouts/AppLayout/AppLayout.tsx | 12 ++ .../src/pages/project/[id]/approval/index.tsx | 27 ++++ .../SecretApprovalListPage.tsx | 73 ++++++++++ .../SecretApprovalListPage/index.tsx | 1 + 7 files changed, 218 insertions(+), 72 deletions(-) create mode 100644 backend/src/models/secretApproval.ts create mode 100644 frontend/src/pages/project/[id]/approval/index.tsx create mode 100644 frontend/src/views/SecretApproval/SecretApprovalListPage/SecretApprovalListPage.tsx create mode 100644 frontend/src/views/SecretApproval/SecretApprovalListPage/index.tsx diff --git a/backend/src/models/secretApproval.ts b/backend/src/models/secretApproval.ts new file mode 100644 index 000000000..f4e2fc92e --- /dev/null +++ b/backend/src/models/secretApproval.ts @@ -0,0 +1,44 @@ +import { Schema, Types, model } from "mongoose"; + +export interface ISecretApproval { + _id: Types.ObjectId; + workspace: Types.ObjectId; + environment: string; + secretPath?: string; + approvers: Types.ObjectId[]; + approvals: number; +} + +const secretApprovalSchema = new Schema( + { + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true + }, + approvers: [ + { + // user associated with the personal secret + type: Schema.Types.ObjectId, + ref: "Membership" + } + ], + environment: { + type: String, + required: true + }, + secretPath: { + type: String, + required: false + }, + approvals: { + type: Number, + default: 1 + } + }, + { + timestamps: true + } +); + +export const SecretApproval = model("SecretApproval", secretApprovalSchema); diff --git a/backend/src/models/secretApprovalRequest.ts b/backend/src/models/secretApprovalRequest.ts index 910ca9135..4133f06a7 100644 --- a/backend/src/models/secretApprovalRequest.ts +++ b/backend/src/models/secretApprovalRequest.ts @@ -1,81 +1,65 @@ -import mongoose, { Schema, model } from "mongoose"; -import { ISecret, Secret } from "./secret"; +import { Schema, Types, model } from "mongoose"; +import { ISecretVersion, SecretVersion } from "../ee/models/secretVersion"; -interface ISecretApprovalRequest { - secret: mongoose.Types.ObjectId; - requestedChanges: ISecret; - requestedBy: mongoose.Types.ObjectId; - approvers: IApprover[]; - status: ApprovalStatus; - timestamp: Date; - requestType: RequestType; - requestId: string; +enum ApprovalStatus { + PENDING = "pending", + APPROVED = "approved", + REJECTED = "rejected" } -interface IApprover { - userId: mongoose.Types.ObjectId; - status: ApprovalStatus; +enum CommitType { + DELETE = "delete", + UPDATE = "update", + CREATE = "create" } -export enum ApprovalStatus { - PENDING = "pending", - APPROVED = "approved", - REJECTED = "rejected" +export interface ISecretApprovalRequest { + _id: Types.ObjectId; + committer: Types.ObjectId; + approvers: { + member: Types.ObjectId; + status: ApprovalStatus; + }[]; + approvals: number; + hasMerged: boolean; + status: ApprovalStatus; + commits: { + secretVersion: Types.ObjectId; + newVersion: ISecretVersion; + op: CommitType; + }[]; } -export enum RequestType { - UPDATE = "update", - DELETE = "delete", - CREATE = "create" -} - -const approverSchema = new mongoose.Schema({ - user: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - required: true, - }, - status: { - type: String, - enum: [ApprovalStatus], - default: ApprovalStatus.PENDING, - }, -}); - -const secretApprovalRequestSchema = new Schema( - { - secret: { - type: mongoose.Schema.Types.ObjectId, - ref: "Secret", - }, - requestedChanges: Secret, - requestedBy: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - }, - approvers: [approverSchema], - status: { - type: String, - enum: ApprovalStatus, - default: ApprovalStatus.PENDING, - }, - timestamp: { - type: Date, - default: Date.now, - }, - requestType: { - type: String, - enum: RequestType, - required: true, - }, - requestId: { - type: String, - required: false, - }, - }, - { - timestamps: true, - } +const secretApprovalSchema = new Schema( + { + approvers: [ + { + member: { + // user associated with the personal secret + type: Schema.Types.ObjectId, + ref: "Membership" + }, + status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING } + } + ], + approvals: { + type: Number, + required: true + }, + hasMerged: { type: Boolean, default: false }, + status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING }, + committer: { type: Schema.Types.ObjectId, ref: "Membership" }, + commits: [ + { + secretVersion: { type: Types.ObjectId, ref: "SecretVersion" }, + newVersion: SecretVersion, + op: { type: String, enum: [CommitType], required: true } + } + ] + }, + { + timestamps: true + } ); -export const SecretApprovalRequest = model("SecretApprovalRequest", secretApprovalRequestSchema); \ No newline at end of file +export const SecretApproval = model("SecretApproval", secretApprovalSchema); diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index f989f2f3a..9900176e1 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -107,6 +107,11 @@ } } }, + "approval": { + "title": "Admin Panel", + "og-title": "Manage your secret change management", + "og-description": "Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files." + }, "integrations": { "title": "Project Integrations", "description": "Manage your integrations of Infisical with third-party services.", diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 05f266f49..d8c522110 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -475,6 +475,18 @@ export const AppLayout = ({ children }: LayoutProps) => { + + + + Secret Change Management + + + { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("approval.title") })} + + + + + +
+ +
+ + ); +}; + +export default SecretApproval; + +SecretApproval.requireAuth = true; diff --git a/frontend/src/views/SecretApproval/SecretApprovalListPage/SecretApprovalListPage.tsx b/frontend/src/views/SecretApproval/SecretApprovalListPage/SecretApprovalListPage.tsx new file mode 100644 index 000000000..3264382fb --- /dev/null +++ b/frontend/src/views/SecretApproval/SecretApprovalListPage/SecretApprovalListPage.tsx @@ -0,0 +1,73 @@ +import { faCheck, faCodeBranch, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { Button, Table, TableContainer, TBody, Td, Th, THead, Tr } from "@app/components/v2"; + +export const SecretApprovalListPage = () => { + return ( +
+
+

Admin Panels

+
+
+
+
+ + 27 Open +
+
+ + 27 Closed +
+
+
+
+
+ 2 secrets added and 1 deleted +
+ + Opened 2 hours ago by akhilmhdh - Review required + +
+
+
+
+
+ Request for secret change +
+
+ + + + + + + + + + + + + + + + + + +
SecretValueCommentTags
TWILIO_SECRET_TOKENvalueSome values-
+
+
+
+ + +
+
+
+ ); +}; diff --git a/frontend/src/views/SecretApproval/SecretApprovalListPage/index.tsx b/frontend/src/views/SecretApproval/SecretApprovalListPage/index.tsx new file mode 100644 index 000000000..71d0f7cf3 --- /dev/null +++ b/frontend/src/views/SecretApproval/SecretApprovalListPage/index.tsx @@ -0,0 +1 @@ +export { SecretApprovalListPage } from "./SecretApprovalListPage"; From edeb6bbc6683ba22610e387b74b88c3e26b75b52 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Wed, 27 Sep 2023 23:10:28 +0530 Subject: [PATCH 2/4] feat(secret-approval): implemented backend api for secret policies --- backend/src/controllers/v1/index.ts | 4 +- .../v1/secretApprovalController.ts | 109 ++++++++++++++++++ backend/src/ee/services/ProjectRoleService.ts | 12 +- backend/src/index.ts | 2 + backend/src/routes/v1/index.ts | 4 +- backend/src/routes/v1/secretApproval.ts | 39 +++++++ backend/src/validation/index.ts | 1 + backend/src/validation/secretApproval.ts | 34 ++++++ 8 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 backend/src/controllers/v1/secretApprovalController.ts create mode 100644 backend/src/routes/v1/secretApproval.ts create mode 100644 backend/src/validation/secretApproval.ts 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() + }) +}); From c67432a56fe72606fcfd9f7c47acee1ded7d24c6 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Wed, 27 Sep 2023 23:10:45 +0530 Subject: [PATCH 3/4] feat(secret-approval): implemented frontend ui for secret policies --- frontend/src/components/v2/Modal/Modal.tsx | 4 +- frontend/src/hooks/api/index.tsx | 1 + .../src/hooks/api/secretApproval/index.tsx | 6 + .../src/hooks/api/secretApproval/mutation.tsx | 58 +++++ .../src/hooks/api/secretApproval/queries.tsx | 36 +++ .../src/hooks/api/secretApproval/types.ts | 31 +++ frontend/src/hooks/api/types.ts | 1 + frontend/src/layouts/AppLayout/AppLayout.tsx | 26 +- .../src/pages/project/[id]/approval/index.tsx | 4 +- .../SecretApprovalListPage.tsx | 73 ------ .../SecretApprovalListPage/index.tsx | 1 - .../SecretApprovalPage/SecretApprovalPage.tsx | 31 +++ .../SecretApprovalPolicyList.tsx | 128 ++++++++++ .../components/SecretApprovalPolicyRow.tsx | 120 +++++++++ .../components/SecretPolicyForm.tsx | 239 ++++++++++++++++++ .../SecretApprovalPolicyList/index.tsx | 1 + .../src/views/SecretApprovalPage/index.tsx | 1 + 17 files changed, 671 insertions(+), 90 deletions(-) create mode 100644 frontend/src/hooks/api/secretApproval/index.tsx create mode 100644 frontend/src/hooks/api/secretApproval/mutation.tsx create mode 100644 frontend/src/hooks/api/secretApproval/queries.tsx create mode 100644 frontend/src/hooks/api/secretApproval/types.ts delete mode 100644 frontend/src/views/SecretApproval/SecretApprovalListPage/SecretApprovalListPage.tsx delete mode 100644 frontend/src/views/SecretApproval/SecretApprovalListPage/index.tsx create mode 100644 frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/index.tsx create mode 100644 frontend/src/views/SecretApprovalPage/index.tsx diff --git a/frontend/src/components/v2/Modal/Modal.tsx b/frontend/src/components/v2/Modal/Modal.tsx index 200ceefb2..97e1f3d3c 100644 --- a/frontend/src/components/v2/Modal/Modal.tsx +++ b/frontend/src/components/v2/Modal/Modal.tsx @@ -22,14 +22,14 @@ export const ModalContent = forwardRef( ) => ( diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index f3a7ab4ca..05de5a8d5 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -7,6 +7,7 @@ export * from "./integrations"; export * from "./keys"; export * from "./organization"; export * from "./roles"; +export * from "./secretApproval"; export * from "./secretFolders"; export * from "./secretImports"; export * from "./secrets"; diff --git a/frontend/src/hooks/api/secretApproval/index.tsx b/frontend/src/hooks/api/secretApproval/index.tsx new file mode 100644 index 000000000..1d4353d9e --- /dev/null +++ b/frontend/src/hooks/api/secretApproval/index.tsx @@ -0,0 +1,6 @@ +export { + useCreateSecretApprovalPolicy, + useDeleteSecretApprovalPolicy, + useUpdateSecretApprovalPolicy +} from "./mutation"; +export { useGetSecretApprovalPolicies } from "./queries"; diff --git a/frontend/src/hooks/api/secretApproval/mutation.tsx b/frontend/src/hooks/api/secretApproval/mutation.tsx new file mode 100644 index 000000000..f171636a0 --- /dev/null +++ b/frontend/src/hooks/api/secretApproval/mutation.tsx @@ -0,0 +1,58 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { secretApprovalKeys } from "./queries"; +import { TCreateSecretPolicyDTO, TDeleteSecretPolicyDTO, TUpdateSecretPolicyDTO } from "./types"; + +export const useCreateSecretApprovalPolicy = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TCreateSecretPolicyDTO>({ + mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath }) => { + const { data } = await apiRequest.post("/api/v1/secret-approvals", { + environment, + workspaceId, + approvals, + approvers, + secretPath + }); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(secretApprovalKeys.getApprovalPolicies(workspaceId)); + } + }); +}; + +export const useUpdateSecretApprovalPolicy = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TUpdateSecretPolicyDTO>({ + mutationFn: async ({ id, approvers, approvals, secretPath }) => { + const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, { + approvals, + approvers, + secretPath + }); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(secretApprovalKeys.getApprovalPolicies(workspaceId)); + } + }); +}; + +export const useDeleteSecretApprovalPolicy = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TDeleteSecretPolicyDTO>({ + mutationFn: async ({ id }) => { + const { data } = await apiRequest.delete(`/api/v1/secret-approvals/${id}`); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(secretApprovalKeys.getApprovalPolicies(workspaceId)); + } + }); +}; diff --git a/frontend/src/hooks/api/secretApproval/queries.tsx b/frontend/src/hooks/api/secretApproval/queries.tsx new file mode 100644 index 000000000..6c176a27a --- /dev/null +++ b/frontend/src/hooks/api/secretApproval/queries.tsx @@ -0,0 +1,36 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TSecretApprovalPolicy } from "./types"; + +export const secretApprovalKeys = { + getApprovalPolicies: (workspaceId: string) => + [{ workspaceId }, "secret-approval-policies"] as const +}; + +const fetchApprovalPolicies = async (workspaceId: string) => { + const { data } = await apiRequest.get<{ approvals: TSecretApprovalPolicy[] }>( + "/api/v1/secret-approvals", + { params: { workspaceId } } + ); + return data.approvals; +}; + +export const useGetSecretApprovalPolicies = ({ + workspaceId, + options = {} +}: { workspaceId: string } & { + options?: UseQueryOptions< + TSecretApprovalPolicy[], + unknown, + TSecretApprovalPolicy[], + ReturnType + >; +}) => + useQuery({ + queryKey: secretApprovalKeys.getApprovalPolicies(workspaceId), + queryFn: () => fetchApprovalPolicies(workspaceId), + ...options, + enabled: Boolean(workspaceId) && (options?.enabled ?? true) + }); diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts new file mode 100644 index 000000000..b77fc1a91 --- /dev/null +++ b/frontend/src/hooks/api/secretApproval/types.ts @@ -0,0 +1,31 @@ +export type TSecretApprovalPolicy = { + _id: string; + workspace: string; + environment: string; + secretPath?: string; + approvers: string[]; + approvals: number; +}; + +export type TCreateSecretPolicyDTO = { + workspaceId: string; + environment: string; + secretPath?: string; + approvers?: string[]; + approvals?: number; +}; + +export type TUpdateSecretPolicyDTO = { + id: string; + approvers?: string[]; + secretPath?: string; + approvals?: number; + // for invalidating list + workspaceId: string; +}; + +export type TDeleteSecretPolicyDTO = { + id: string; + // for invalidating list + workspaceId: string; +}; diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index 098078090..c91fb551d 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -4,6 +4,7 @@ export type { IntegrationAuth } from "./integrationAuth/types"; export type { TCloudIntegration, TIntegration } from "./integrations/types"; export type { UserWsKeyPair } from "./keys/types"; export type { Organization } from "./organization/types"; +export type { TSecretApprovalPolicy } from "./secretApproval/types"; export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types"; export type { SubscriptionPlan } from "./subscriptions/types"; export type { WsTag } from "./tags/types"; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index d8c522110..8dd5f12ab 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -475,18 +475,20 @@ export const AppLayout = ({ children }: LayoutProps) => {
- - - - Secret Change Management - - - + {process.env.NEXT_PUBLIC_SECRET_APPROVAL === "true" && ( + + + + Admin Panel + + + + )} { const { t } = useTranslation(); @@ -16,7 +16,7 @@ const SecretApproval = () => {
- +
); diff --git a/frontend/src/views/SecretApproval/SecretApprovalListPage/SecretApprovalListPage.tsx b/frontend/src/views/SecretApproval/SecretApprovalListPage/SecretApprovalListPage.tsx deleted file mode 100644 index 3264382fb..000000000 --- a/frontend/src/views/SecretApproval/SecretApprovalListPage/SecretApprovalListPage.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { faCheck, faCodeBranch, faXmark } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { Button, Table, TableContainer, TBody, Td, Th, THead, Tr } from "@app/components/v2"; - -export const SecretApprovalListPage = () => { - return ( -
-
-

Admin Panels

-
-
-
-
- - 27 Open -
-
- - 27 Closed -
-
-
-
-
- 2 secrets added and 1 deleted -
- - Opened 2 hours ago by akhilmhdh - Review required - -
-
-
-
-
- Request for secret change -
-
- - - - - - - - - - - - - - - - - - -
SecretValueCommentTags
TWILIO_SECRET_TOKENvalueSome values-
-
-
-
- - -
-
-
- ); -}; diff --git a/frontend/src/views/SecretApproval/SecretApprovalListPage/index.tsx b/frontend/src/views/SecretApproval/SecretApprovalListPage/index.tsx deleted file mode 100644 index 71d0f7cf3..000000000 --- a/frontend/src/views/SecretApproval/SecretApprovalListPage/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { SecretApprovalListPage } from "./SecretApprovalListPage"; diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx new file mode 100644 index 000000000..35243e75f --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -0,0 +1,31 @@ +import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; + +import { SecretApprovalPolicyList } from "./components/SecretApprovalPolicyList"; + +enum TabSection { + ApprovalRequests = "approval-requests", + Rules = "approval-rules" +} + +export const SecretApprovalPage = () => { + const { currentWorkspace } = useWorkspace(); + const workspaceId = currentWorkspace?._id || ""; + + return ( +
+
+

Admin Panels

+
+ + + Secret PRs + Policies + + + + + +
+ ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx new file mode 100644 index 000000000..dcc27e49f --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx @@ -0,0 +1,128 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { + Button, + DeleteActionModal, + Table, + TableContainer, + TableSkeleton, + TBody, + Th, + THead, + Tr +} from "@app/components/v2"; +import { usePopUp } from "@app/hooks"; +import { + useDeleteSecretApprovalPolicy, + useGetSecretApprovalPolicies, + useGetWorkspaceUsers +} from "@app/hooks/api"; +import { TSecretApprovalPolicy } from "@app/hooks/api/types"; + +import { SecretApprovalPolicyRow } from "./components/SecretApprovalPolicyRow"; +import { SecretPolicyForm } from "./components/SecretPolicyForm"; + +type Props = { + workspaceId: string; +}; + +export const SecretApprovalPolicyList = ({ workspaceId }: Props) => { + const { handlePopUpToggle, handlePopUpOpen, handlePopUpClose, popUp } = usePopUp([ + "secretPolicyForm", + "deletePolicy" + ] as const); + const { createNotification } = useNotificationContext(); + + const { data: members } = useGetWorkspaceUsers(workspaceId); + const { data: policies, isLoading: isPoliciesLoading } = useGetSecretApprovalPolicies({ + workspaceId + }); + + const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy(); + + const handleDeletePolicy = async () => { + const { _id: id } = popUp.deletePolicy.data as TSecretApprovalPolicy; + try { + await deleteSecretApprovalPolicy({ + workspaceId, + id + }); + createNotification({ + type: "success", + text: "Successfully deleted policy" + }); + handlePopUpClose("deletePolicy"); + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "Failed to delete policy" + }); + } + }; + + return ( +
+
+
+ Approval Policies +
+ Implement policies to prevent unauthorized secret changes. +
+
+
+ +
+
+ + + + + + + + + + + + {isPoliciesLoading && ( + + )} + {policies?.map((policy) => ( + handlePopUpOpen("secretPolicyForm", policy)} + onDelete={() => handlePopUpOpen("deletePolicy", policy)} + /> + ))} + +
EnvironmentSecret PathEligible ApproversApproval Required +
+
+ handlePopUpToggle("secretPolicyForm", isOpen)} + members={members} + editValues={popUp.secretPolicyForm.data as TSecretApprovalPolicy} + /> + handlePopUpToggle("deletePolicy", isOpen)} + onDeleteApproved={handleDeletePolicy} + /> +
+ ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx new file mode 100644 index 000000000..c39ba2b69 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx @@ -0,0 +1,120 @@ +import { useState } from "react"; +import { faCheckCircle, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + IconButton, + Input, + Td, + Tooltip, + Tr +} from "@app/components/v2"; +import { useUpdateSecretApprovalPolicy } from "@app/hooks/api"; +import { TSecretApprovalPolicy } from "@app/hooks/api/types"; +import { TWorkspaceUser } from "@app/hooks/api/users/types"; + +type Props = { + policy: TSecretApprovalPolicy; + members?: TWorkspaceUser[]; + workspaceId: string; + onEdit: () => void; + onDelete: () => void; +}; + +export const SecretApprovalPolicyRow = ({ + policy, + members = [], + workspaceId, + onEdit, + onDelete +}: Props) => { + const [selectedApprovers, setSelectedApprovers] = useState([]); + const { mutate: updateSecretApprovalPolicy, isLoading } = useUpdateSecretApprovalPolicy(); + + return ( + + {policy.environment} + {policy.secretPath || "*"} + + { + if (!isOpen) { + updateSecretApprovalPolicy( + { + workspaceId, + id: policy._id, + approvers: selectedApprovers + }, + { + onSettled: () => { + setSelectedApprovers([]); + } + } + ); + } else { + setSelectedApprovers(policy.approvers); + } + }} + > + + + + + Select members that must approve changes + {members?.map(({ _id, user }) => { + const isChecked = selectedApprovers.includes(_id); + return ( + { + evt.preventDefault(); + setSelectedApprovers((state) => + isChecked ? state.filter((el) => el !== _id) : [...state, _id] + ); + }} + key={`create-policy-members-${_id}`} + iconPos="right" + icon={isChecked && } + > + {user.email} + + ); + })} + + + + {policy.approvals} + +
+ + + + + + + + + + +
+ + + ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx new file mode 100644 index 000000000..78d1e3fbc --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx @@ -0,0 +1,239 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { faCheckCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useCreateSecretApprovalPolicy, useUpdateSecretApprovalPolicy } from "@app/hooks/api"; +import { TSecretApprovalPolicy } from "@app/hooks/api/types"; +import { TWorkspaceUser } from "@app/hooks/api/users/types"; + +type Props = { + isOpen?: boolean; + onToggle: (isOpen: boolean) => void; + members?: TWorkspaceUser[]; + workspaceId: string; + editValues?: TSecretApprovalPolicy; +}; + +const formSchema = z.object({ + environment: z.string(), + secretPath: z.string().optional(), + approvals: z.number().min(1), + approvers: z.string().array().optional() +}); + +type TFormSchema = z.infer; + +export const SecretPolicyForm = ({ + isOpen, + onToggle, + members = [], + workspaceId, + editValues +}: Props) => { + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema), + values: editValues + }); + const { currentWorkspace } = useWorkspace(); + const { createNotification } = useNotificationContext(); + + const environments = currentWorkspace?.environments || []; + useEffect(() => { + if (!isOpen) reset({}); + }, [isOpen]); + + const isEditMode = Boolean(editValues); + + const { mutateAsync: createSecretApprovalPolicy } = useCreateSecretApprovalPolicy(); + const { mutateAsync: updateSecretApprovalPolicy } = useUpdateSecretApprovalPolicy(); + + const handleCreatePolicy = async (data: TFormSchema) => { + try { + await createSecretApprovalPolicy({ + ...data, + workspaceId + }); + createNotification({ + type: "success", + text: "Successfully created policy" + }); + onToggle(false); + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "Failed to create policy" + }); + } + }; + + const handleUpdatePolicy = async (data: TFormSchema) => { + if (!editValues?._id) return; + try { + await updateSecretApprovalPolicy({ + id: editValues?._id, + ...data, + secretPath: data.secretPath ?? "-", + workspaceId + }); + createNotification({ + type: "success", + text: "Successfully updated policy" + }); + onToggle(false); + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "failed to update policy" + }); + } + }; + + const handleFormSubmit = async (data: TFormSchema) => { + if (isEditMode) { + await handleUpdatePolicy(data); + } else { + await handleCreatePolicy(data); + } + }; + + return ( + + +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + + + + Select members that must approve changes + {members.map(({ _id, user }) => { + const isChecked = value?.includes(_id); + return ( + { + evt.preventDefault(); + onChange( + isChecked + ? value?.filter((el) => el !== _id) + : [...(value || []), _id] + ); + }} + key={`create-policy-members-${_id}`} + iconPos="right" + icon={isChecked && } + > + {user.email} + + ); + })} + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ +
+
+ ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/index.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/index.tsx new file mode 100644 index 000000000..f204264b4 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/index.tsx @@ -0,0 +1 @@ +export { SecretApprovalPolicyList } from "./SecretApprovalPolicyList"; diff --git a/frontend/src/views/SecretApprovalPage/index.tsx b/frontend/src/views/SecretApprovalPage/index.tsx new file mode 100644 index 000000000..e45406427 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/index.tsx @@ -0,0 +1 @@ +export { SecretApprovalPage } from "./SecretApprovalPage"; From b0c398688bc8bf584d3af8fdbdd338edaaf9b315 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Thu, 28 Sep 2023 12:21:37 +0530 Subject: [PATCH 4/4] feat(secret-approval): updated names to secret policy and fixed approval number bug --- backend/src/controllers/v1/index.ts | 4 ++-- ...r.ts => secretApprovalPolicyController.ts} | 24 +++++++++---------- backend/src/index.ts | 4 ++-- ...retApproval.ts => secretApprovalPolicy.ts} | 9 ++++--- backend/src/models/secretApprovalRequest.ts | 7 ++++-- backend/src/routes/v1/index.ts | 4 ++-- ...retApproval.ts => secretApprovalPolicy.ts} | 10 ++++---- backend/src/validation/secretApproval.ts | 4 ++-- .../src/hooks/api/secretApproval/types.ts | 4 ++-- .../SecretApprovalPolicyList.tsx | 9 ++++++- .../components/SecretPolicyForm.tsx | 12 ++++++---- 11 files changed, 54 insertions(+), 37 deletions(-) rename backend/src/controllers/v1/{secretApprovalController.ts => secretApprovalPolicyController.ts} (73%) rename backend/src/models/{secretApproval.ts => secretApprovalPolicy.ts} (74%) rename backend/src/routes/v1/{secretApproval.ts => secretApprovalPolicy.ts} (62%) diff --git a/backend/src/controllers/v1/index.ts b/backend/src/controllers/v1/index.ts index 9dcbb12c2..c2bb1b94d 100644 --- a/backend/src/controllers/v1/index.ts +++ b/backend/src/controllers/v1/index.ts @@ -16,7 +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"; +import * as secretApprovalPolicyController from "./secretApprovalPolicyController"; export { authController, @@ -37,5 +37,5 @@ export { secretScanningController, webhookController, secretImpsController, - secretApprovalController + secretApprovalPolicyController }; diff --git a/backend/src/controllers/v1/secretApprovalController.ts b/backend/src/controllers/v1/secretApprovalPolicyController.ts similarity index 73% rename from backend/src/controllers/v1/secretApprovalController.ts rename to backend/src/controllers/v1/secretApprovalPolicyController.ts index 8441a93b0..245932353 100644 --- a/backend/src/controllers/v1/secretApprovalController.ts +++ b/backend/src/controllers/v1/secretApprovalPolicyController.ts @@ -6,13 +6,13 @@ import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService"; import { validateRequest } from "../../helpers/validation"; -import { SecretApproval } from "../../models/secretApproval"; +import { SecretApprovalPolicy } from "../../models/secretApprovalPolicy"; 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) => { +export const createSecretApprovalPolicy = async (req: Request, res: Response) => { const { body: { approvals, secretPath, approvers, environment, workspaceId } } = await validateRequest(reqValidator.CreateSecretApprovalRule, req); @@ -23,7 +23,7 @@ export const createSecretApprovalRule = async (req: Request, res: Response) => { ProjectPermissionSub.SecretApproval ); - const secretApproval = new SecretApproval({ + const secretApproval = new SecretApprovalPolicy({ workspace: workspaceId, secretPath, environment, @@ -37,13 +37,13 @@ export const createSecretApprovalRule = async (req: Request, res: Response) => { }); }; -export const updateSecretApprovalRule = async (req: Request, res: Response) => { +export const updateSecretApprovalPolicy = async (req: Request, res: Response) => { const { body: { approvals, approvers, secretPath }, params: { id } } = await validateRequest(reqValidator.UpdateSecretApprovalRule, req); - const secretApproval = await SecretApproval.findById(id); + const secretApproval = await SecretApprovalPolicy.findById(id); if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND; const { permission } = await getUserProjectPermissions( @@ -55,10 +55,10 @@ export const updateSecretApprovalRule = async (req: Request, res: Response) => { ProjectPermissionSub.SecretApproval ); - const updatedDoc = await SecretApproval.findByIdAndUpdate(id, { + const updatedDoc = await SecretApprovalPolicy.findByIdAndUpdate(id, { approvals, approvers, - $set: secretPath === "-" ? undefined : { secretPath } + ...(secretPath === null ? { $unset: { secretPath: 1 } } : { secretPath }) }); return res.send({ @@ -66,12 +66,12 @@ export const updateSecretApprovalRule = async (req: Request, res: Response) => { }); }; -export const deleteSecretApprovalRule = async (req: Request, res: Response) => { +export const deleteSecretApprovalPolicy = async (req: Request, res: Response) => { const { params: { id } } = await validateRequest(reqValidator.DeleteSecretApprovalRule, req); - const secretApproval = await SecretApproval.findById(id); + const secretApproval = await SecretApprovalPolicy.findById(id); if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND; const { permission } = await getUserProjectPermissions( @@ -83,14 +83,14 @@ export const deleteSecretApprovalRule = async (req: Request, res: Response) => { ProjectPermissionSub.SecretApproval ); - const deletedDoc = await SecretApproval.findByIdAndDelete(id); + const deletedDoc = await SecretApprovalPolicy.findByIdAndDelete(id); return res.send({ approval: deletedDoc }); }; -export const getSecretApprovalRules = async (req: Request, res: Response) => { +export const getSecretApprovalPolicy = async (req: Request, res: Response) => { const { query: { workspaceId } } = await validateRequest(reqValidator.GetSecretApprovalRuleList, req); @@ -101,7 +101,7 @@ export const getSecretApprovalRules = async (req: Request, res: Response) => { ProjectPermissionSub.SecretApproval ); - const doc = await SecretApproval.find({ workspace: workspaceId }); + const doc = await SecretApprovalPolicy.find({ workspace: workspaceId }); return res.send({ approvals: doc diff --git a/backend/src/index.ts b/backend/src/index.ts index c3aed4b7b..2a5159738 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -38,7 +38,7 @@ import { membership as v1MembershipRouter, organization as v1OrganizationRouter, password as v1PasswordRouter, - secretApproval as v1SecretApproval, + secretApprovalPolicy as v1SecretApprovalPolicy, secretImps as v1SecretImpsRouter, secret as v1SecretRouter, secretsFolder as v1SecretsFolder, @@ -177,7 +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); + app.use("/api/v1/secret-approvals", v1SecretApprovalPolicy); // v2 routes (improvements) app.use("/api/v2/signup", v2SignupRouter); diff --git a/backend/src/models/secretApproval.ts b/backend/src/models/secretApprovalPolicy.ts similarity index 74% rename from backend/src/models/secretApproval.ts rename to backend/src/models/secretApprovalPolicy.ts index f4e2fc92e..9471f82db 100644 --- a/backend/src/models/secretApproval.ts +++ b/backend/src/models/secretApprovalPolicy.ts @@ -1,6 +1,6 @@ import { Schema, Types, model } from "mongoose"; -export interface ISecretApproval { +export interface ISecretApprovalPolicy { _id: Types.ObjectId; workspace: Types.ObjectId; environment: string; @@ -9,7 +9,7 @@ export interface ISecretApproval { approvals: number; } -const secretApprovalSchema = new Schema( +const secretApprovalPolicySchema = new Schema( { workspace: { type: Schema.Types.ObjectId, @@ -41,4 +41,7 @@ const secretApprovalSchema = new Schema( } ); -export const SecretApproval = model("SecretApproval", secretApprovalSchema); +export const SecretApprovalPolicy = model( + "SecretApprovalPolicy", + secretApprovalPolicySchema +); diff --git a/backend/src/models/secretApprovalRequest.ts b/backend/src/models/secretApprovalRequest.ts index 4133f06a7..a145b5773 100644 --- a/backend/src/models/secretApprovalRequest.ts +++ b/backend/src/models/secretApprovalRequest.ts @@ -30,7 +30,7 @@ export interface ISecretApprovalRequest { }[]; } -const secretApprovalSchema = new Schema( +const secretApprovalRequestSchema = new Schema( { approvers: [ { @@ -62,4 +62,7 @@ const secretApprovalSchema = new Schema( } ); -export const SecretApproval = model("SecretApproval", secretApprovalSchema); +export const SecretApprovalRequest = model( + "SecretApprovalRequest", + secretApprovalRequestSchema +); diff --git a/backend/src/routes/v1/index.ts b/backend/src/routes/v1/index.ts index 7c0f94972..cfdccdc92 100644 --- a/backend/src/routes/v1/index.ts +++ b/backend/src/routes/v1/index.ts @@ -17,7 +17,7 @@ import integrationAuth from "./integrationAuth"; import secretsFolder from "./secretsFolder"; import webhooks from "./webhook"; import secretImps from "./secretImps"; -import secretApproval from "./secretApproval"; +import secretApprovalPolicy from "./secretApprovalPolicy"; export { signup, @@ -39,5 +39,5 @@ export { secretsFolder, webhooks, secretImps, - secretApproval + secretApprovalPolicy }; diff --git a/backend/src/routes/v1/secretApproval.ts b/backend/src/routes/v1/secretApprovalPolicy.ts similarity index 62% rename from backend/src/routes/v1/secretApproval.ts rename to backend/src/routes/v1/secretApprovalPolicy.ts index 213bb3cc3..51c2aa3e7 100644 --- a/backend/src/routes/v1/secretApproval.ts +++ b/backend/src/routes/v1/secretApprovalPolicy.ts @@ -1,7 +1,7 @@ import express from "express"; const router = express.Router(); import { requireAuth } from "../../middleware"; -import { secretApprovalController } from "../../controllers/v1"; +import { secretApprovalPolicyController } from "../../controllers/v1"; import { AuthMode } from "../../variables"; router.get( @@ -9,7 +9,7 @@ router.get( requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - secretApprovalController.getSecretApprovalRules + secretApprovalPolicyController.getSecretApprovalPolicy ); router.post( @@ -17,7 +17,7 @@ router.post( requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - secretApprovalController.createSecretApprovalRule + secretApprovalPolicyController.createSecretApprovalPolicy ); router.patch( @@ -25,7 +25,7 @@ router.patch( requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - secretApprovalController.updateSecretApprovalRule + secretApprovalPolicyController.updateSecretApprovalPolicy ); router.delete( @@ -33,7 +33,7 @@ router.delete( requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - secretApprovalController.deleteSecretApprovalRule + secretApprovalPolicyController.deleteSecretApprovalPolicy ); export default router; diff --git a/backend/src/validation/secretApproval.ts b/backend/src/validation/secretApproval.ts index a25da81b2..9d4b10ce5 100644 --- a/backend/src/validation/secretApproval.ts +++ b/backend/src/validation/secretApproval.ts @@ -10,7 +10,7 @@ export const CreateSecretApprovalRule = z.object({ body: z.object({ workspaceId: z.string(), environment: z.string(), - secretPath: z.string().optional(), + secretPath: z.string().optional().nullable(), approvers: z.string().array().optional(), approvals: z.number().min(1).default(1) }) @@ -23,7 +23,7 @@ export const UpdateSecretApprovalRule = z.object({ body: z.object({ approvers: z.string().array().optional(), approvals: z.number().min(1).optional(), - secretPath: z.string().optional() + secretPath: z.string().optional().nullable() }) }); diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts index b77fc1a91..8eb486664 100644 --- a/frontend/src/hooks/api/secretApproval/types.ts +++ b/frontend/src/hooks/api/secretApproval/types.ts @@ -10,7 +10,7 @@ export type TSecretApprovalPolicy = { export type TCreateSecretPolicyDTO = { workspaceId: string; environment: string; - secretPath?: string; + secretPath?: string | null; approvers?: string[]; approvals?: number; }; @@ -18,7 +18,7 @@ export type TCreateSecretPolicyDTO = { export type TUpdateSecretPolicyDTO = { id: string; approvers?: string[]; - secretPath?: string; + secretPath?: string | null; approvals?: number; // for invalidating list workspaceId: string; diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx index dcc27e49f..700b1a785 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx @@ -1,14 +1,16 @@ -import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { faFileShield, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { Button, DeleteActionModal, + EmptyState, Table, TableContainer, TableSkeleton, TBody, + Td, Th, THead, Tr @@ -96,6 +98,11 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => { {isPoliciesLoading && ( )} + {!isPoliciesLoading && !policies?.length && ( + + + + )} {policies?.map((policy) => ( ( - + )} /> @@ -220,7 +220,11 @@ export const SecretPolicyForm = ({ isError={Boolean(error)} errorText={error?.message} > - + field.onChange(parseInt(el.target.value, 10))} + /> )} />