From 1d2f10178f59267743b5b815f3dd9dcdf041be18 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 29 Mar 2024 16:24:15 +0100 Subject: [PATCH 001/158] Draft --- .../src/ee/services/license/licence-fns.ts | 2 +- frontend/src/components/basic/Divider.tsx | 15 ++++++++ .../permissions/PermissionDeniedBanner.tsx | 36 ++++++++++--------- frontend/src/layouts/AppLayout/AppLayout.tsx | 4 +-- .../SecretApprovalPage/SecretApprovalPage.tsx | 22 +++++++----- 5 files changed, 51 insertions(+), 28 deletions(-) create mode 100644 frontend/src/components/basic/Divider.tsx diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 189a3c4e0..492d09ec3 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ status: null, trial_end: null, has_used_trial: true, - secretApproval: false, + secretApproval: true, secretRotation: true }); diff --git a/frontend/src/components/basic/Divider.tsx b/frontend/src/components/basic/Divider.tsx new file mode 100644 index 000000000..f1a570d7b --- /dev/null +++ b/frontend/src/components/basic/Divider.tsx @@ -0,0 +1,15 @@ +import { twMerge } from "tailwind-merge"; + +interface IProps { + className?: string; +} + +const Divider = ({ className }: IProps): JSX.Element => { + return ( +
+ + ); +}; + +export default Divider; diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx index 067ee9c4a..f9707d634 100644 --- a/frontend/src/components/permissions/PermissionDeniedBanner.tsx +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -3,6 +3,8 @@ import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; +import { Button } from "../v2"; + type Props = { containerClassName?: string; className?: string; @@ -17,24 +19,24 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children containerClassName )} > -
-
- -
-
-
Access Restricted
- {children || ( -
- Your role has limited permissions, please
contact your administrator to gain - access -
- )} +
+
+
+ +
+
+
Access Restricted
+ {children || ( +
+ Your role has limited permissions, please
contact your administrator to gain + access +
+ )} +
+
); diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 2550150f3..a6c25c7c2 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -115,7 +115,7 @@ type TAddProjectFormData = yup.InferType; export const AppLayout = ({ children }: LayoutProps) => { const router = useRouter(); - + const { mutateAsync } = useGetOrgTrialUrl(); const { workspaces, currentWorkspace } = useWorkspace(); @@ -554,7 +554,7 @@ export const AppLayout = ({ children }: LayoutProps) => { } icon="system-outline-189-domain-verification" > - Secret Approvals + Approvals {Boolean(secretApprovalReqCount?.open) && ( {secretApprovalReqCount?.open} diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index ce1896efa..4ab5830fe 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Divider from "@app/components/basic/Divider"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { useWorkspace } from "@app/context"; @@ -9,8 +10,10 @@ import { SecretApprovalPolicyList } from "./components/SecretApprovalPolicyList" import { SecretApprovalRequest } from "./components/SecretApprovalRequest"; enum TabSection { - ApprovalRequests = "approval-requests", - Rules = "approval-rules" + SecretApprovalRequests = "approval-requests", + SecretPolicies = "approval-rules", + ResourcePolicies = "resource-rules", + ResourceApprovalRequests = "resource-requests" } export const SecretApprovalPage = () => { @@ -21,7 +24,7 @@ export const SecretApprovalPage = () => {
-

Secret Approval Workflows

+

Approval Workflows

Create approval policies for any modifications to secrets in sensitive environments and folders. @@ -39,15 +42,18 @@ export const SecretApprovalPage = () => {

- + - Secret PRs - Policies + Secret Approvals + Secret Policies + + Resource Approvals + Resource Policies - + - + From 12c655a152e0e5d9e752ad5579333ad6e8d64277 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 1 Apr 2024 08:22:46 -0700 Subject: [PATCH 002/158] Feat: Request Access --- backend/src/@types/fastify.d.ts | 2 ++ backend/src/@types/knex.d.ts | 19 +++++++++++++++++++ backend/src/db/schemas/index.ts | 2 ++ backend/src/db/schemas/models.ts | 6 ++++++ backend/src/ee/routes/v1/index.ts | 3 +++ .../server/plugins/auth/inject-identity.ts | 8 ++++++++ 6 files changed, 40 insertions(+) diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index b3e9d9952..12c07486f 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -1,6 +1,7 @@ import "fastify"; import { TUsers } from "@app/db/schemas"; +import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service"; import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service"; @@ -113,6 +114,7 @@ declare module "fastify" { identityAccessToken: TIdentityAccessTokenServiceFactory; identityProject: TIdentityProjectServiceFactory; identityUa: TIdentityUaServiceFactory; + accessApprovalPolicy: TAccessApprovalPolicyServiceFactory; secretApprovalPolicy: TSecretApprovalPolicyServiceFactory; secretApprovalRequest: TSecretApprovalRequestServiceFactory; secretRotation: TSecretRotationServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index a7d76e944..b6e8744ed 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -2,6 +2,12 @@ import { Knex } from "knex"; import { TableName, + TAccessApprovalPolicies, + TAccessApprovalPoliciesApprovers, + TAccessApprovalPoliciesApproversInsert, + TAccessApprovalPoliciesApproversUpdate, + TAccessApprovalPoliciesInsert, + TAccessApprovalPoliciesUpdate, TApiKeys, TApiKeysInsert, TApiKeysUpdate, @@ -344,6 +350,19 @@ declare module "knex/types/tables" { TIdentityProjectAdditionalPrivilegeInsert, TIdentityProjectAdditionalPrivilegeUpdate >; + + [TableName.AccessApprovalPolicy]: Knex.CompositeTableType< + TAccessApprovalPolicies, + TAccessApprovalPoliciesInsert, + TAccessApprovalPoliciesUpdate + >; + + [TableName.AccessApprovalPolicyApprover]: Knex.CompositeTableType< + TAccessApprovalPoliciesApprovers, + TAccessApprovalPoliciesApproversInsert, + TAccessApprovalPoliciesApproversUpdate + >; + [TableName.ScimToken]: Knex.CompositeTableType; [TableName.SecretApprovalPolicy]: Knex.CompositeTableType< TSecretApprovalPolicies, diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 0eb9b1986..f02188577 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -1,3 +1,5 @@ +export * from "./access-approval-policies"; +export * from "./access-approval-policies-approvers"; export * from "./api-keys"; export * from "./audit-log-streams"; export * from "./audit-logs"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 3baa7f40d..8e66cabe4 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -50,12 +50,18 @@ export enum TableName { IdentityProjectMembershipRole = "identity_project_membership_role", IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege", ScimToken = "scim_tokens", + + // New tables so far + AccessApprovalPolicy = "access_approval_policies", + AccessApprovalPolicyApprover = "access_approval_policies_approvers", + SecretApprovalPolicy = "secret_approval_policies", SecretApprovalPolicyApprover = "secret_approval_policies_approvers", SecretApprovalRequest = "secret_approval_requests", SecretApprovalRequestReviewer = "secret_approval_requests_reviewers", SecretApprovalRequestSecret = "secret_approval_requests_secrets", SecretApprovalRequestSecretTag = "secret_approval_request_secret_tags", + SecretRotation = "secret_rotations", SecretRotationOutput = "secret_rotation_outputs", SamlConfig = "saml_configs", diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index cf325b2e3..086a1b3d6 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,4 +1,5 @@ import { registerAuditLogStreamRouter } from "./audit-log-stream-router"; +import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router"; import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerGroupRouter } from "./group-router"; @@ -41,6 +42,8 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { prefix: "/secret-rotation-providers" }); + await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals" }); + await server.register( async (dynamicSecretRouter) => { await dynamicSecretRouter.register(registerDynamicSecretRouter); diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 4c0683797..71522dcc2 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -108,8 +108,16 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { if (req.url.includes("/api/v3/auth/")) { return; } + + console.log("authMode", authMode); + console.log("authMode", authMode); + console.log("authMode", authMode); + console.log("authMode", authMode); + if (!authMode) return; + console.log("authMode", authMode); + switch (authMode) { case AuthMode.JWT: { const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token); From 91bf6a6dad439a3ead05fbd9932ca5ff0b070ca1 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 1 Apr 2024 08:23:02 -0700 Subject: [PATCH 003/158] Fix: Remove logs --- backend/src/server/plugins/auth/inject-identity.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 71522dcc2..d8814dd40 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -109,15 +109,8 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { return; } - console.log("authMode", authMode); - console.log("authMode", authMode); - console.log("authMode", authMode); - console.log("authMode", authMode); - if (!authMode) return; - console.log("authMode", authMode); - switch (authMode) { case AuthMode.JWT: { const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token); From 68a11db1c6914f4987ca0d022ec18331a32c286c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 1 Apr 2024 09:49:22 -0700 Subject: [PATCH 004/158] Feat: Request access --- backend/scripts/generate-schema-types.ts | 2 +- .../20240330075122_access-approval-policy.ts | 38 +++ .../access-approval-policies-approvers.ts | 25 ++ .../db/schemas/access-approval-policies.ts | 23 ++ .../v1/access-approval-policy-router.ts | 133 ++++++++++ .../access-approval-policy-approver-dal.ts | 10 + .../access-approval-policy-dal.ts | 76 ++++++ .../access-approval-policy-service.ts | 206 +++++++++++++++ .../access-approval-policy-types.ts | 27 ++ backend/src/server/routes/index.ts | 18 ++ .../src/hooks/api/accessApproval/index.tsx | 6 + .../src/hooks/api/accessApproval/mutation.tsx | 58 ++++ .../src/hooks/api/accessApproval/queries.tsx | 40 +++ .../src/hooks/api/accessApproval/types.ts | 45 ++++ frontend/src/hooks/api/index.tsx | 1 + frontend/src/hooks/api/types.ts | 17 +- .../SecretApprovalPage/SecretApprovalPage.tsx | 9 +- .../AccessApprovalPolicyList.tsx | 166 ++++++++++++ .../components/AccessApprovalPolicyRow.tsx | 145 ++++++++++ .../components/AccessPolicyForm.tsx | 250 ++++++++++++++++++ .../AccessApprovalPolicyList/index.tsx | 1 + 21 files changed, 1285 insertions(+), 11 deletions(-) create mode 100644 backend/src/db/migrations/20240330075122_access-approval-policy.ts create mode 100644 backend/src/db/schemas/access-approval-policies-approvers.ts create mode 100644 backend/src/db/schemas/access-approval-policies.ts create mode 100644 backend/src/ee/routes/v1/access-approval-policy-router.ts create mode 100644 backend/src/ee/services/access-approval-policy/access-approval-policy-approver-dal.ts create mode 100644 backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts create mode 100644 backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts create mode 100644 backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts create mode 100644 frontend/src/hooks/api/accessApproval/index.tsx create mode 100644 frontend/src/hooks/api/accessApproval/mutation.tsx create mode 100644 frontend/src/hooks/api/accessApproval/queries.tsx create mode 100644 frontend/src/hooks/api/accessApproval/types.ts create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/index.tsx diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 8c913991f..28b736152 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -5,7 +5,7 @@ import knex from "knex"; import { writeFileSync } from "fs"; dotenv.config({ - path: path.join(__dirname, "../../.env.migration") + path: path.join(__dirname, "../../.env") }); const db = knex({ diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts new file mode 100644 index 000000000..0595d5c6e --- /dev/null +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -0,0 +1,38 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.AccessApprovalPolicy))) { + await knex.schema.createTable(TableName.AccessApprovalPolicy, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("name").notNullable(); + t.integer("approvals").defaultTo(1).notNullable(); + t.uuid("envId").notNullable(); + t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + } + await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); + + if (!(await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover))) { + await knex.schema.createTable(TableName.AccessApprovalPolicyApprover, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("approverId").notNullable(); + t.foreign("approverId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE"); + t.uuid("policyId").notNullable(); + t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + } + + await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); + await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover); + await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); + await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); +} diff --git a/backend/src/db/schemas/access-approval-policies-approvers.ts b/backend/src/db/schemas/access-approval-policies-approvers.ts new file mode 100644 index 000000000..4ebbfa9ae --- /dev/null +++ b/backend/src/db/schemas/access-approval-policies-approvers.ts @@ -0,0 +1,25 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const AccessApprovalPoliciesApproversSchema = z.object({ + id: z.string().uuid(), + approverId: z.string().uuid(), + policyId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TAccessApprovalPoliciesApprovers = z.infer; +export type TAccessApprovalPoliciesApproversInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TAccessApprovalPoliciesApproversUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/access-approval-policies.ts b/backend/src/db/schemas/access-approval-policies.ts new file mode 100644 index 000000000..550049702 --- /dev/null +++ b/backend/src/db/schemas/access-approval-policies.ts @@ -0,0 +1,23 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const AccessApprovalPoliciesSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + approvals: z.number().default(1), + envId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TAccessApprovalPolicies = z.infer; +export type TAccessApprovalPoliciesInsert = Omit, TImmutableDBKeys>; +export type TAccessApprovalPoliciesUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/ee/routes/v1/access-approval-policy-router.ts b/backend/src/ee/routes/v1/access-approval-policy-router.ts new file mode 100644 index 000000000..ec5331a89 --- /dev/null +++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts @@ -0,0 +1,133 @@ +import { nanoid } from "nanoid"; +import { z } from "zod"; + +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { sapPubSchema } from "@app/server/routes/sanitizedSchemas"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/", + method: "POST", + schema: { + body: z + .object({ + workspaceId: z.string(), + name: z.string().optional(), + environment: z.string(), + approvers: z.string().array().min(1), + approvals: z.number().min(1).default(1) + }) + .refine((data) => data.approvals <= data.approvers.length, { + path: ["approvals"], + message: "The number of approvals should be lower than the number of approvers." + }), + response: { + 200: z.object({ + approval: sapPubSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const approval = await server.services.accessApprovalPolicy.createAccessApprovalPolicy({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.body.workspaceId, + ...req.body, + name: req.body.name ?? `${req.body.environment}-${nanoid(3)}` + }); + return { approval }; + } + }); + + server.route({ + url: "/", + method: "GET", + schema: { + querystring: z.object({ + workspaceId: z.string().trim() + }), + response: { + 200: z.object({ + approvals: sapPubSchema.merge(z.object({ approvers: z.string().array() })).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const approvals = await server.services.accessApprovalPolicy.getAccessApprovalPolicyByProjectId({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.query.workspaceId + }); + return { approvals }; + } + }); + + server.route({ + url: "/:policyId", + method: "PATCH", + schema: { + params: z.object({ + policyId: z.string() + }), + body: z + .object({ + name: z.string().optional(), + approvers: z.string().array().min(1), + approvals: z.number().min(1).default(1) + }) + .refine((data) => data.approvals <= data.approvers.length, { + path: ["approvals"], + message: "The number of approvals should be lower than the number of approvers." + }), + response: { + 200: z.object({ + approval: sapPubSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + await server.services.accessApprovalPolicy.updateAccessApprovalPolicy({ + policyId: req.params.policyId, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + ...req.body + }); + } + }); + + server.route({ + url: "/:policyId", + method: "DELETE", + schema: { + params: z.object({ + policyId: z.string() + }), + response: { + 200: z.object({ + approval: sapPubSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const approval = await server.services.accessApprovalPolicy.deleteAccessApprovalPolicy({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + policyId: req.params.policyId + }); + return { approval }; + } + }); +}; diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-approver-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-approver-dal.ts new file mode 100644 index 000000000..e14854d8f --- /dev/null +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-approver-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TAccessApprovalPolicyApproverDALFactory = ReturnType; + +export const accessApprovalPolicyApproverDALFactory = (db: TDbClient) => { + const accessApprovalPolicyApproverOrm = ormify(db, TableName.AccessApprovalPolicyApprover); + return { ...accessApprovalPolicyApproverOrm }; +}; diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts new file mode 100644 index 000000000..792a851c3 --- /dev/null +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts @@ -0,0 +1,76 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName, TAccessApprovalPolicies } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { buildFindFilter, mergeOneToManyRelation, ormify, selectAllTableCols, TFindFilter } from "@app/lib/knex"; + +export type TAccessApprovalPolicyDALFactory = ReturnType; + +export const accessApprovalPolicyDALFactory = (db: TDbClient) => { + const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy); + + const sapFindQuery = async (tx: Knex, filter: TFindFilter) => { + const result = await tx(TableName.AccessApprovalPolicy) + // eslint-disable-next-line + .where(buildFindFilter(filter)) + .join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) + .join( + TableName.AccessApprovalPolicyApprover, + `${TableName.AccessApprovalPolicy}.id`, + `${TableName.AccessApprovalPolicyApprover}.policyId` + ) + .select(tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover)) + .select(tx.ref("name").withSchema(TableName.Environment).as("envName")) + .select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug")) + .select(tx.ref("id").withSchema(TableName.Environment).as("envId")) + .select(tx.ref("projectId").withSchema(TableName.Environment)) + .select(selectAllTableCols(TableName.AccessApprovalPolicy)); + + return result; + }; + + const findById = async (id: string, tx?: Knex) => { + try { + const doc = await sapFindQuery(tx || db, { + [`${TableName.AccessApprovalPolicy}.id` as "id"]: id + }); + const formatedDoc = mergeOneToManyRelation( + doc, + "id", + ({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({ + ...el, + envId, + environment: { id: envId, name, slug } + }), + ({ approverId }) => approverId, + "approvers" + ); + return formatedDoc?.[0]; + } catch (error) { + throw new DatabaseError({ error, name: "FindById" }); + } + }; + + const find = async (filter: TFindFilter, tx?: Knex) => { + try { + const docs = await sapFindQuery(tx || db, filter); + const formatedDoc = mergeOneToManyRelation( + docs, + "id", + ({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({ + ...el, + envId, + environment: { id: envId, name, slug } + }), + ({ approverId }) => approverId, + "approvers" + ); + return formatedDoc; + } catch (error) { + throw new DatabaseError({ error, name: "Find" }); + } + }; + + return { ...accessApprovalPolicyOrm, find, findById }; +}; diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts new file mode 100644 index 000000000..d1f0ce1e7 --- /dev/null +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -0,0 +1,206 @@ +import { ForbiddenError } from "@casl/ability"; + +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 { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; +import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; + +import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal"; +import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal"; +import { + TCreateAccessApprovalPolicy, + TDeleteAccessApprovalPolicy, + TListAccessApprovalPoliciesDTO, + TUpdateAccessApprovalPolicy +} from "./access-approval-policy-types"; + +type TSecretApprovalPolicyServiceFactoryDep = { + permissionService: Pick; + accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory; + projectEnvDAL: Pick; + accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory; + projectMembershipDAL: Pick; +}; + +export type TAccessApprovalPolicyServiceFactory = ReturnType; + +export const accessApprovalPolicyServiceFactory = ({ + accessApprovalPolicyDAL, + accessApprovalPolicyApproverDAL, + permissionService, + projectEnvDAL, + projectMembershipDAL +}: TSecretApprovalPolicyServiceFactoryDep) => { + const createAccessApprovalPolicy = async ({ + name, + actor, + actorId, + actorOrgId, + actorAuthMethod, + approvals, + approvers, + projectId, + environment + }: TCreateAccessApprovalPolicy) => { + if (approvals > approvers.length) + throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.SecretApproval + ); + const env = await projectEnvDAL.findOne({ slug: environment, projectId }); + if (!env) throw new BadRequestError({ message: "Environment not found" }); + + const secretApprovers = await projectMembershipDAL.find({ + projectId, + $in: { id: approvers } + }); + if (secretApprovers.length !== approvers.length) { + throw new BadRequestError({ message: "Approver not found in project" }); + } + + const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => { + const doc = await accessApprovalPolicyDAL.create( + { + envId: env.id, + approvals, + name + }, + tx + ); + await accessApprovalPolicyApproverDAL.insertMany( + secretApprovers.map(({ id }) => ({ + approverId: id, + policyId: doc.id + })), + tx + ); + return doc; + }); + return { ...accessApproval, environment: env, projectId }; + }; + + const getAccessApprovalPolicyByProjectId = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + projectId + }: TListAccessApprovalPoliciesDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + + const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId }); + return accessApprovalPolicies; + }; + + const updateAccessApprovalPolicy = async ({ + policyId, + approvers, + name, + actorId, + actor, + actorOrgId, + actorAuthMethod, + approvals + }: TUpdateAccessApprovalPolicy) => { + const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId); + if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + accessApprovalPolicy.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); + + const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { + const doc = await accessApprovalPolicyDAL.updateById( + accessApprovalPolicy.id, + { + approvals, + name + }, + tx + ); + if (approvers) { + // Find the workspace project memberships of the users passed in the approvers array + const secretApprovers = await projectMembershipDAL.find( + { + projectId: accessApprovalPolicy.projectId, + $in: { id: approvers } + }, + { tx } + ); + if (secretApprovers.length !== approvers.length) + throw new BadRequestError({ message: "Approver not found in project" }); + if (doc.approvals > secretApprovers.length) + throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); + await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx); + await accessApprovalPolicyApproverDAL.insertMany( + secretApprovers.map(({ id }) => ({ + approverId: id, + policyId: doc.id + })), + tx + ); + } + return doc; + }); + return { + ...updatedPolicy, + environment: accessApprovalPolicy.environment, + projectId: accessApprovalPolicy.projectId + }; + }; + + const deleteAccessApprovalPolicy = async ({ + policyId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TDeleteAccessApprovalPolicy) => { + const policy = await accessApprovalPolicyDAL.findById(policyId); + if (!policy) throw new BadRequestError({ message: "Secret approval policy not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + policy.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.SecretApproval + ); + + await accessApprovalPolicyDAL.deleteById(policyId); + return policy; + }; + + return { + createAccessApprovalPolicy, + deleteAccessApprovalPolicy, + updateAccessApprovalPolicy, + getAccessApprovalPolicyByProjectId + }; +}; diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts new file mode 100644 index 000000000..034132d04 --- /dev/null +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts @@ -0,0 +1,27 @@ +import { TProjectPermission } from "@app/lib/types"; + +export type TCreateAccessApprovalPolicy = { + approvals: number; + environment: string; + approvers: string[]; + projectId: string; + name: string; +} & Omit; + +export type TUpdateAccessApprovalPolicy = { + policyId: string; + approvals?: number; + approvers: string[]; + name?: string; +} & Omit; + +export type TDeleteAccessApprovalPolicy = { + policyId: string; +} & Omit; + +export type TListAccessApprovalPoliciesDTO = TProjectPermission; + +export type TGetBoardAccessApprovalPolicy = { + projectId: string; + environment: string; +} & Omit; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index aeb66d93f..0a2c77c9f 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2,6 +2,9 @@ import { Knex } from "knex"; import { z } from "zod"; import { registerV1EERoutes } from "@app/ee/routes/v1"; +import { accessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal"; +import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal"; +import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service"; import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal"; import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue"; import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; @@ -207,6 +210,10 @@ export const registerRoutes = async ( const scimDAL = scimDALFactory(db); const ldapConfigDAL = ldapConfigDALFactory(db); const ldapGroupMapDAL = ldapGroupMapDALFactory(db); + + const accessApprovalPolicyDAL = accessApprovalPolicyDALFactory(db); + const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db); + const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db); const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db); const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db); @@ -265,6 +272,15 @@ export const registerRoutes = async ( secretApprovalPolicyDAL }); const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL }); + + const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({ + accessApprovalPolicyDAL, + accessApprovalPolicyApproverDAL, + permissionService, + projectEnvDAL, + projectMembershipDAL + }); + const samlService = samlConfigServiceFactory({ permissionService, orgBotDAL, @@ -596,6 +612,7 @@ export const registerRoutes = async ( secretVersionTagDAL, secretQueueService }); + const secretRotationQueue = secretRotationQueueFactory({ telemetryService, secretRotationDAL, @@ -732,6 +749,7 @@ export const registerRoutes = async ( identityProject: identityProjectService, identityUa: identityUaService, secretApprovalPolicy: sapService, + accessApprovalPolicy: accessApprovalPolicyService, secretApprovalRequest: sarService, secretRotation: secretRotationService, dynamicSecret: dynamicSecretService, diff --git a/frontend/src/hooks/api/accessApproval/index.tsx b/frontend/src/hooks/api/accessApproval/index.tsx new file mode 100644 index 000000000..537dfe3be --- /dev/null +++ b/frontend/src/hooks/api/accessApproval/index.tsx @@ -0,0 +1,6 @@ +export { + useCreateAccessApprovalPolicy, + useDeleteAccessApprovalPolicy, + useUpdateAccessApprovalPolicy +} from "./mutation"; +export { useGetAccessApprovalPolicies } from "./queries"; diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx new file mode 100644 index 000000000..98e9b3f97 --- /dev/null +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -0,0 +1,58 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { accessApprovalKeys } from "./queries"; +import { TCreateAccessPolicyDTO, TDeleteSecretPolicyDTO, TUpdateAccessPolicyDTO } from "./types"; + +export const useCreateAccessApprovalPolicy = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TCreateAccessPolicyDTO>({ + mutationFn: async ({ environment, workspaceId, approvals, approvers, name }) => { + const { data } = await apiRequest.post("/api/v1/access-approvals", { + environment, + workspaceId, + approvals, + approvers, + name + }); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(workspaceId)); + } + }); +}; + +export const useUpdateAccessApprovalPolicy = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TUpdateAccessPolicyDTO>({ + mutationFn: async ({ id, approvers, approvals, name }) => { + const { data } = await apiRequest.patch(`/api/v1/access-approvals/${id}`, { + approvals, + approvers, + name + }); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(workspaceId)); + } + }); +}; + +export const useDeleteAccessApprovalPolicy = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TDeleteSecretPolicyDTO>({ + mutationFn: async ({ id }) => { + const { data } = await apiRequest.delete(`/api/v1/access-approvals/${id}`); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(workspaceId)); + } + }); +}; diff --git a/frontend/src/hooks/api/accessApproval/queries.tsx b/frontend/src/hooks/api/accessApproval/queries.tsx new file mode 100644 index 000000000..864122593 --- /dev/null +++ b/frontend/src/hooks/api/accessApproval/queries.tsx @@ -0,0 +1,40 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TAccessApprovalPolicy, TGetSecretApprovalPoliciesDTO } from "./types"; + +export const accessApprovalKeys = { + getAccessApprovalPolicies: (workspaceId: string) => + [{ workspaceId }, "access-approval-policies"] as const, + getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) => [ + { workspaceId, environment }, + "access-approval-policy" + ] +}; + +const fetchApprovalPolicies = async (workspaceId: string) => { + const { data } = await apiRequest.get<{ approvals: TAccessApprovalPolicy[] }>( + "/api/v1/access-approvals", + { params: { workspaceId } } + ); + return data.approvals; +}; + +export const useGetAccessApprovalPolicies = ({ + workspaceId, + options = {} +}: TGetSecretApprovalPoliciesDTO & { + options?: UseQueryOptions< + TAccessApprovalPolicy[], + unknown, + TAccessApprovalPolicy[], + ReturnType + >; +}) => + useQuery({ + queryKey: accessApprovalKeys.getAccessApprovalPolicies(workspaceId), + queryFn: () => fetchApprovalPolicies(workspaceId), + ...options, + enabled: Boolean(workspaceId) && (options?.enabled ?? true) + }); diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts new file mode 100644 index 000000000..63020166b --- /dev/null +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -0,0 +1,45 @@ +import { WorkspaceEnv } from "../workspace/types"; + +export type TAccessApprovalPolicy = { + id: string; + name: string; + approvals: number; + envId: string; + workspace: string; + environment: WorkspaceEnv; + projectId: string; + approvers: string[]; +}; + +export type TGetSecretApprovalPoliciesDTO = { + workspaceId: string; +}; + +export type TGetSecretApprovalPolicyOfBoardDTO = { + workspaceId: string; + environment: string; + secretPath: string; +}; + +export type TCreateAccessPolicyDTO = { + workspaceId: string; + name?: string; + environment: string; + approvers?: string[]; + approvals?: number; +}; + +export type TUpdateAccessPolicyDTO = { + id: string; + name?: string; + approvers?: 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/index.tsx b/frontend/src/hooks/api/index.tsx index 574da5a31..61e8cb666 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -1,3 +1,4 @@ +export * from "./accessApproval"; export * from "./admin"; export * from "./apiKeys"; export * from "./auditLogs"; diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index 49949d88e..516a5d7cf 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -1,5 +1,6 @@ import { ZodIssue } from "zod"; +export type { TAccessApprovalPolicy } from "./accessApproval/types"; export type { TAuditLogStream } from "./auditLogStreams/types"; export type { GetAuthTokenAPI } from "./auth/types"; export type { IncidentContact } from "./incidentContacts/types"; @@ -49,13 +50,13 @@ export enum ApiErrorTypes { export type TApiErrors = | { - error: ApiErrorTypes.ValidationError; - message: ZodIssue[]; - statusCode: 403; - } + error: ApiErrorTypes.ValidationError; + message: ZodIssue[]; + statusCode: 403; + } | { error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 401 } | { - statusCode: 400; - message: string; - error: ApiErrorTypes.BadRequestError; - }; + statusCode: 400; + message: string; + error: ApiErrorTypes.BadRequestError; + }; diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 4ab5830fe..ebbfd7b5e 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -6,6 +6,7 @@ import Divider from "@app/components/basic/Divider"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { useWorkspace } from "@app/context"; +import { AccessApprovalPolicyList } from "./components/AccessApprovalPolicyList"; import { SecretApprovalPolicyList } from "./components/SecretApprovalPolicyList"; import { SecretApprovalRequest } from "./components/SecretApprovalRequest"; @@ -47,8 +48,8 @@ export const SecretApprovalPage = () => { Secret Approvals Secret Policies - Resource Approvals - Resource Policies + Access Approvals + Access Policies @@ -56,6 +57,10 @@ export const SecretApprovalPage = () => { + + + +
); diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx new file mode 100644 index 000000000..834b54f00 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx @@ -0,0 +1,166 @@ +import { faFileShield, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + DeleteActionModal, + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr, + UpgradePlanModal +} from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useSubscription +} from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { useDeleteAccessApprovalPolicy, useGetWorkspaceUsers } from "@app/hooks/api"; +import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries"; +import { TAccessApprovalPolicy } from "@app/hooks/api/types"; + +import { AccessApprovalPolicyRow } from "./components/AccessApprovalPolicyRow"; +import { AccessPolicyForm } from "./components/AccessPolicyForm"; + +interface IProps { + workspaceId: string; +} + +export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => { + const { handlePopUpToggle, handlePopUpOpen, handlePopUpClose, popUp } = usePopUp([ + "secretPolicyForm", + "deletePolicy", + "upgradePlan" + ] as const); + const { permission } = useProjectPermission(); + const { subscription } = useSubscription(); + + const { data: members } = useGetWorkspaceUsers(workspaceId); + const { data: policies, isLoading: isPoliciesLoading } = useGetAccessApprovalPolicies({ + workspaceId, + options: { + enabled: permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) + } + }); + + const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteAccessApprovalPolicy(); + + const handleDeletePolicy = async () => { + const { id } = popUp.deletePolicy.data as TAccessApprovalPolicy; + 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 ( +
+
+
+ Access Approval Policies +
+ Implement policies to prevent unauthorized secret changes. +
+
+
+ + {(isAllowed) => ( + + )} + +
+
+ + + + + + + + + + + + {isPoliciesLoading && ( + + )} + {!isPoliciesLoading && !policies?.length && ( + + + + )} + {policies?.map((policy) => ( + handlePopUpOpen("secretPolicyForm", policy)} + onDelete={() => handlePopUpOpen("deletePolicy", policy)} + /> + ))} + +
NameEnvironmentSecret PathApproval Required +
+ +
+
+ handlePopUpToggle("secretPolicyForm", isOpen)} + members={members} + editValues={popUp.secretPolicyForm.data as TAccessApprovalPolicy} + /> + handlePopUpToggle("deletePolicy", isOpen)} + onDeleteApproved={handleDeletePolicy} + /> + handlePopUpToggle("upgradePlan", isOpen)} + text="You can add secret approval policy if you switch to Infisical's Enterprise plan." + /> +
+ ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx new file mode 100644 index 000000000..54eaae67c --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx @@ -0,0 +1,145 @@ +import { useState } from "react"; +import { faCheckCircle, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + IconButton, + Input, + Td, + Tr +} from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { useUpdateAccessApprovalPolicy } from "@app/hooks/api"; +import { TAccessApprovalPolicy } from "@app/hooks/api/types"; +import { TWorkspaceUser } from "@app/hooks/api/users/types"; + +type Props = { + policy: TAccessApprovalPolicy; + members?: TWorkspaceUser[]; + workspaceId: string; + onEdit: () => void; + onDelete: () => void; +}; + +export const AccessApprovalPolicyRow = ({ + policy, + members = [], + workspaceId, + onEdit, + onDelete +}: Props) => { + const [selectedApprovers, setSelectedApprovers] = useState([]); + const { mutate: updateAccessApprovalPolicy, isLoading } = useUpdateAccessApprovalPolicy(); + const { permission } = useProjectPermission(); + + return ( + + {policy.name} + {policy.environment.slug} + + { + if (!isOpen) { + updateAccessApprovalPolicy( + { + workspaceId, + id: policy.id, + approvers: selectedApprovers + }, + { + onSettled: () => { + setSelectedApprovers([]); + } + } + ); + } else { + setSelectedApprovers(policy.approvers); + } + }} + > + + + + + + Select members that are allowed to 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} + +
+ + {(isAllowed) => ( + + + + )} + + + {(isAllowed) => ( + + + + )} + +
+ + + ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx new file mode 100644 index 000000000..56a4bdef5 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -0,0 +1,250 @@ +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 { createNotification } from "@app/components/notifications"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { + useCreateAccessApprovalPolicy, + useUpdateAccessApprovalPolicy +} from "@app/hooks/api/accessApproval"; +import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types"; +import { TWorkspaceUser } from "@app/hooks/api/users/types"; + +type Props = { + isOpen?: boolean; + onToggle: (isOpen: boolean) => void; + members?: TWorkspaceUser[]; + workspaceId: string; + editValues?: TAccessApprovalPolicy; +}; + +const formSchema = z + .object({ + environment: z.string(), + name: z.string().optional(), + secretPath: z.string().optional().nullable(), + approvals: z.number().min(1), + approvers: z.string().array().min(1) + }) + .refine((data) => data.approvals <= data.approvers.length, { + path: ["approvals"], + message: "The number of approvals should be lower than the number of approvers." + }); + +type TFormSchema = z.infer; + +export const AccessPolicyForm = ({ + isOpen, + onToggle, + members = [], + workspaceId, + editValues +}: Props) => { + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema), + values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined + }); + const { currentWorkspace } = useWorkspace(); + + const environments = currentWorkspace?.environments || []; + useEffect(() => { + if (!isOpen) reset({}); + }, [isOpen]); + + const isEditMode = Boolean(editValues); + + const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy(); + const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy(); + + const handleCreatePolicy = async (data: TFormSchema) => { + try { + await createAccessApprovalPolicy({ + ...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 updateAccessApprovalPolicy({ + id: editValues?.id, + ...data, + 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 are allowed to 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} + + ); + })} + + + + )} + /> + ( + + field.onChange(parseInt(el.target.value, 10))} + /> + + )} + /> +
+ + +
+ +
+
+ ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/index.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/index.tsx new file mode 100644 index 000000000..f6db07c94 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/index.tsx @@ -0,0 +1 @@ +export { AccessApprovalPolicyList } from "./AccessApprovalPolicyList"; From d33711880326a5698305a5e5b51b88ab0b11283e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 2 Apr 2024 22:25:58 -0700 Subject: [PATCH 005/158] Feat: Request access --- backend/src/@types/fastify.d.ts | 2 ++ backend/src/@types/knex.d.ts | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 12c07486f..71a7c887c 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -2,6 +2,7 @@ import "fastify"; import { TUsers } from "@app/db/schemas"; import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service"; +import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service"; import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service"; @@ -115,6 +116,7 @@ declare module "fastify" { identityProject: TIdentityProjectServiceFactory; identityUa: TIdentityUaServiceFactory; accessApprovalPolicy: TAccessApprovalPolicyServiceFactory; + accessApprovalRequest: TAccessApprovalRequestServiceFactory; secretApprovalPolicy: TSecretApprovalPolicyServiceFactory; secretApprovalRequest: TSecretApprovalRequestServiceFactory; secretRotation: TSecretRotationServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index b6e8744ed..14ebd4295 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -8,6 +8,12 @@ import { TAccessApprovalPoliciesApproversUpdate, TAccessApprovalPoliciesInsert, TAccessApprovalPoliciesUpdate, + TAccessApprovalRequests, + TAccessApprovalRequestsInsert, + TAccessApprovalRequestsReviewers, + TAccessApprovalRequestsReviewersInsert, + TAccessApprovalRequestsReviewersUpdate, + TAccessApprovalRequestsUpdate, TApiKeys, TApiKeysInsert, TApiKeysUpdate, @@ -363,6 +369,18 @@ declare module "knex/types/tables" { TAccessApprovalPoliciesApproversUpdate >; + [TableName.AccessApprovalRequest]: Knex.CompositeTableType< + TAccessApprovalRequests, + TAccessApprovalRequestsInsert, + TAccessApprovalRequestsUpdate + >; + + [TableName.AccessApprovalRequestReviewer]: Knex.CompositeTableType< + TAccessApprovalRequestsReviewers, + TAccessApprovalRequestsReviewersInsert, + TAccessApprovalRequestsReviewersUpdate + >; + [TableName.ScimToken]: Knex.CompositeTableType; [TableName.SecretApprovalPolicy]: Knex.CompositeTableType< TSecretApprovalPolicies, From a528d011c0c3f8999dfc6bdfc2009ea46a08d0f4 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:35:51 -0700 Subject: [PATCH 006/158] Feat: Request Access (migrations) --- .../20240330075122_access-approval-policy.ts | 3 +- ...20240401173320_access_approval_requests.ts | 51 +++++++++++++++++++ .../db/schemas/access-approval-policies.ts | 1 + .../access-approval-requests-reviewers.ts | 26 ++++++++++ .../db/schemas/access-approval-requests.ts | 26 ++++++++++ backend/src/db/schemas/index.ts | 2 + 6 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 backend/src/db/migrations/20240401173320_access_approval_requests.ts create mode 100644 backend/src/db/schemas/access-approval-requests-reviewers.ts create mode 100644 backend/src/db/schemas/access-approval-requests.ts diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index 0595d5c6e..8203fb333 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -10,6 +10,7 @@ export async function up(knex: Knex): Promise { t.string("name").notNullable(); t.integer("approvals").defaultTo(1).notNullable(); t.uuid("envId").notNullable(); + t.string("secretPath"); t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); t.timestamps(true, true, true); }); @@ -31,8 +32,8 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover); + await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); } diff --git a/backend/src/db/migrations/20240401173320_access_approval_requests.ts b/backend/src/db/migrations/20240401173320_access_approval_requests.ts new file mode 100644 index 000000000..901be9a78 --- /dev/null +++ b/backend/src/db/migrations/20240401173320_access_approval_requests.ts @@ -0,0 +1,51 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.AccessApprovalRequest))) { + await knex.schema.createTable(TableName.AccessApprovalRequest, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.uuid("policyId").notNullable(); + t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE"); + + t.uuid("privilegeId").nullable(); + t.foreign("privilegeId").references("id").inTable(TableName.ProjectUserAdditionalPrivilege).onDelete("CASCADE"); + + t.uuid("requestedBy").notNullable(); + t.foreign("requestedBy").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE"); + + // We use these values to create the actual privilege at a later point in time. + t.boolean("isTemporary").notNullable(); + t.string("temporaryRange").nullable(); + + t.jsonb("permissions").notNullable(); + + t.timestamps(true, true, true); + }); + } + await createOnUpdateTrigger(knex, TableName.AccessApprovalRequest); + + if (!(await knex.schema.hasTable(TableName.AccessApprovalRequestReviewer))) { + await knex.schema.createTable(TableName.AccessApprovalRequestReviewer, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("member").notNullable(); + t.foreign("member").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE"); + t.string("status").notNullable(); + t.uuid("requestId").notNullable(); + t.foreign("requestId").references("id").inTable(TableName.AccessApprovalRequest).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + } + await createOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.AccessApprovalRequestReviewer); + await knex.schema.dropTableIfExists(TableName.AccessApprovalRequest); + + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); +} diff --git a/backend/src/db/schemas/access-approval-policies.ts b/backend/src/db/schemas/access-approval-policies.ts index 550049702..bf7e74ff2 100644 --- a/backend/src/db/schemas/access-approval-policies.ts +++ b/backend/src/db/schemas/access-approval-policies.ts @@ -12,6 +12,7 @@ export const AccessApprovalPoliciesSchema = z.object({ name: z.string(), approvals: z.number().default(1), envId: z.string().uuid(), + secretPath: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date() }); diff --git a/backend/src/db/schemas/access-approval-requests-reviewers.ts b/backend/src/db/schemas/access-approval-requests-reviewers.ts new file mode 100644 index 000000000..509fd7425 --- /dev/null +++ b/backend/src/db/schemas/access-approval-requests-reviewers.ts @@ -0,0 +1,26 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const AccessApprovalRequestsReviewersSchema = z.object({ + id: z.string().uuid(), + member: z.string().uuid(), + status: z.string(), + requestId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TAccessApprovalRequestsReviewers = z.infer; +export type TAccessApprovalRequestsReviewersInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TAccessApprovalRequestsReviewersUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/access-approval-requests.ts b/backend/src/db/schemas/access-approval-requests.ts new file mode 100644 index 000000000..bd598bac6 --- /dev/null +++ b/backend/src/db/schemas/access-approval-requests.ts @@ -0,0 +1,26 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const AccessApprovalRequestsSchema = z.object({ + id: z.string().uuid(), + policyId: z.string().uuid(), + privilegeId: z.string().uuid().nullable().optional(), + requestedBy: z.string().uuid(), + isTemporary: z.boolean(), + temporaryRange: z.string().nullable().optional(), + permissions: z.unknown(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TAccessApprovalRequests = z.infer; +export type TAccessApprovalRequestsInsert = Omit, TImmutableDBKeys>; +export type TAccessApprovalRequestsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index f02188577..0fd7c5271 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -1,5 +1,7 @@ export * from "./access-approval-policies"; export * from "./access-approval-policies-approvers"; +export * from "./access-approval-requests"; +export * from "./access-approval-requests-reviewers"; export * from "./api-keys"; export * from "./audit-log-streams"; export * from "./audit-logs"; From f981c59b5c259b47cf6ef97dc4e38e585049c82b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:36:11 -0700 Subject: [PATCH 007/158] Feat: Request access (models) --- backend/src/db/schemas/models.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 8e66cabe4..f401e822c 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -54,6 +54,8 @@ export enum TableName { // New tables so far AccessApprovalPolicy = "access_approval_policies", AccessApprovalPolicyApprover = "access_approval_policies_approvers", + AccessApprovalRequest = "access_approval_requests", + AccessApprovalRequestReviewer = "access_approval_requests_reviewers", SecretApprovalPolicy = "secret_approval_policies", SecretApprovalPolicyApprover = "secret_approval_policies_approvers", From 99f5ed1f4bec5bff9e972286ad24d33940ca640c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:36:47 -0700 Subject: [PATCH 008/158] Fix: Move to project slug --- .../v1/access-approval-policy-router.ts | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-policy-router.ts b/backend/src/ee/routes/v1/access-approval-policy-router.ts index ec5331a89..8a8090042 100644 --- a/backend/src/ee/routes/v1/access-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts @@ -12,8 +12,9 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi schema: { body: z .object({ - workspaceId: z.string(), + projectSlug: z.string().trim(), name: z.string().optional(), + secretPath: z.string().trim().default("/"), environment: z.string(), approvers: z.string().array().min(1), approvals: z.number().min(1).default(1) @@ -35,8 +36,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.body.workspaceId, ...req.body, + projectSlug: req.body.projectSlug, name: req.body.name ?? `${req.body.environment}-${nanoid(3)}` }); return { approval }; @@ -48,11 +49,11 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi method: "GET", schema: { querystring: z.object({ - workspaceId: z.string().trim() + projectSlug: z.string().trim() }), response: { 200: z.object({ - approvals: sapPubSchema.merge(z.object({ approvers: z.string().array() })).array() + approvals: sapPubSchema.extend({ approvers: z.string().array(), secretPath: z.string().optional() }).array() }) } }, @@ -63,12 +64,41 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.query.workspaceId + projectSlug: req.query.projectSlug }); return { approvals }; } }); + server.route({ + url: "/policy-count", + method: "GET", + schema: { + querystring: z.object({ + projectSlug: z.string(), + envSlug: z.string() + }), + response: { + 200: z.object({ + policyCount: z.number() + }) + } + }, + + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { policyCount } = await server.services.accessApprovalPolicy.getAccessPolicyCountByEnvSlug({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + projectSlug: req.query.projectSlug, + actorOrgId: req.permission.orgId, + envSlug: req.query.envSlug + }); + return { policyCount }; + } + }); + server.route({ url: "/:policyId", method: "PATCH", @@ -79,6 +109,11 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi body: z .object({ name: z.string().optional(), + secretPath: z + .string() + .trim() + .optional() + .transform((val) => (val === "" ? "/" : val)), approvers: z.string().array().min(1), approvals: z.number().min(1).default(1) }) From f458e34c3734f797f50317447d21bfa979bd3e38 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:37:49 -0700 Subject: [PATCH 009/158] Feat: Request access (new routes) --- .../v1/access-approval-request-router.ts | 176 ++++++++++++++++++ backend/src/ee/routes/v1/index.ts | 2 + 2 files changed, 178 insertions(+) create mode 100644 backend/src/ee/routes/v1/access-approval-request-router.ts diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts new file mode 100644 index 000000000..696c8b76f --- /dev/null +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -0,0 +1,176 @@ +import slugify from "@sindresorhus/slugify"; +import { z } from "zod"; + +import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas"; +import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/", + method: "POST", + schema: { + body: z.object({ + slug: z + .string() + .min(1) + .max(60) + .trim() + .default(`requested-privilege-${slugify(alphaNumericNanoId(12))}`) + .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }), + permissions: z.any().array(), + isTemporary: z.boolean(), + temporaryRange: z.string().optional() + }), + querystring: z.object({ + projectSlug: z.string().trim(), + secretPath: z.string().trim(), + envSlug: z.string().trim() + }), + response: { + 200: z.object({ + approval: AccessApprovalRequestsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { request } = await server.services.accessApprovalRequest.createAccessApprovalRequest({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + permissions: req.body.permissions, + envSlug: req.query.envSlug, + actorOrgId: req.permission.orgId, + secretPath: req.query.secretPath, + projectSlug: req.query.projectSlug, + temporaryRange: req.body.temporaryRange, + isTemporary: req.body.isTemporary + }); + return { approval: request }; + } + }); + + server.route({ + url: "/count", + method: "GET", + schema: { + querystring: z.object({ + projectSlug: z.string().trim() + }), + response: { + 200: z.object({ + pendingCount: z.number(), + finalizedCount: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { count } = await server.services.accessApprovalRequest.getCount({ + projectSlug: req.query.projectSlug, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); + + return { ...count }; + } + }); + + server.route({ + url: "/", + method: "GET", + schema: { + querystring: z.object({ + projectSlug: z.string().trim(), + authorProjectMembershipId: z.string().trim().optional(), + envSlug: z.string().trim().optional() + }), + response: { + 200: z.object({ + requests: AccessApprovalRequestsSchema.extend({ + environmentName: z.string(), + isApproved: z.boolean(), + privilege: z + .object({ + membershipId: z.string(), + isTemporary: z.boolean(), + temporaryMode: z.string().nullish(), + temporaryRange: z.string().nullish(), + temporaryAccessStartTime: z.date().nullish(), + temporaryAccessEndTime: z.date().nullish(), + permissions: z.unknown() + }) + .nullable(), + policy: z.object({ + id: z.string(), + name: z.string(), + approvals: z.number(), + approvers: z.string().array(), + secretPath: z.string().nullish(), + envId: z.string() + }), + reviewers: z + .object({ + member: z.string(), + status: z.string() + }) + .array() + }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({ + projectSlug: req.query.projectSlug, + authorProjectMembershipId: req.query.authorProjectMembershipId, + envSlug: req.query.envSlug, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); + + return { requests }; + } + }); + + server.route({ + url: "/:requestId/review", + method: "POST", + schema: { + params: z.object({ + requestId: z.string().trim() + }), + body: z.object({ + status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]) + }), + response: { + 200: z.object({ + review: AccessApprovalRequestsReviewersSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const review = await server.services.accessApprovalRequest.reviewAccessRequest({ + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + requestId: req.params.requestId, + status: req.body.status + }); + + return { review }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 086a1b3d6..c73ed24c5 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,5 +1,6 @@ import { registerAuditLogStreamRouter } from "./audit-log-stream-router"; import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router"; +import { registerAccessApprovalRequestRouter } from "./access-approval-request-router"; import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerGroupRouter } from "./group-router"; @@ -43,6 +44,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { }); await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals" }); + await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approval-requests" }); await server.register( async (dynamicSecretRouter) => { From 7e29a6a6561825c8c4ff401a5f998767130c18c2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:38:36 -0700 Subject: [PATCH 010/158] Fix: Access Approval Policy DAL bugs --- .../access-approval-policy/access-approval-policy-dal.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts index 792a851c3..88e288832 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts @@ -10,7 +10,7 @@ export type TAccessApprovalPolicyDALFactory = ReturnType { const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy); - const sapFindQuery = async (tx: Knex, filter: TFindFilter) => { + const accessApprovalPolicyFindQuery = async (tx: Knex, filter: TFindFilter) => { const result = await tx(TableName.AccessApprovalPolicy) // eslint-disable-next-line .where(buildFindFilter(filter)) @@ -32,7 +32,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const doc = await sapFindQuery(tx || db, { + const doc = await accessApprovalPolicyFindQuery(tx || db, { [`${TableName.AccessApprovalPolicy}.id` as "id"]: id }); const formatedDoc = mergeOneToManyRelation( @@ -54,7 +54,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { const find = async (filter: TFindFilter, tx?: Knex) => { try { - const docs = await sapFindQuery(tx || db, filter); + const docs = await accessApprovalPolicyFindQuery(tx || db, filter); const formatedDoc = mergeOneToManyRelation( docs, "id", @@ -66,7 +66,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { ({ approverId }) => approverId, "approvers" ); - return formatedDoc; + return formatedDoc.map((policy) => ({ ...policy, secretPath: policy.secretPath || undefined })); } catch (error) { throw new DatabaseError({ error, name: "Find" }); } From eca36f19934eec05945f25fa6c24eced1e29c0c3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:38:52 -0700 Subject: [PATCH 011/158] Feat: Request access --- .../access-approval-policy-fns.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts new file mode 100644 index 000000000..b570c32fa --- /dev/null +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts @@ -0,0 +1,37 @@ +import { ForbiddenError, subject } from "@casl/ability"; + +import { BadRequestError } from "@app/lib/errors"; +import { ActorType } from "@app/services/auth/auth-type"; + +import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { TVerifyApprovers } from "./access-approval-policy-types"; + +export const verifyApprovers = async ({ + approverProjectMemberships, + projectId, + orgId, + envSlug, + actorAuthMethod, + secretPath, + permissionService +}: TVerifyApprovers) => { + for (const approver of approverProjectMemberships) { + try { + // eslint-disable-next-line no-await-in-loop + const { permission: approverPermission } = await permissionService.getProjectPermission( + ActorType.USER, + approver.userId, + projectId, + actorAuthMethod, + orgId + ); + + ForbiddenError.from(approverPermission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath }) + ); + } catch (err) { + throw new BadRequestError({ message: "One or more approvers doesn't have access to be specified secret path" }); + } + } +}; From 5255c4075ae6f54e95cdb864bf7ab5a93c7304b3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:39:15 -0700 Subject: [PATCH 012/158] Fix: Validate approvers access --- .../access-approval-policy-service.ts | 87 ++++++++++++++++--- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index d1f0ce1e7..30f5f10fe 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -3,22 +3,26 @@ import { ForbiddenError } from "@casl/ability"; 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 { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal"; import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal"; +import { verifyApprovers } from "./access-approval-policy-fns"; import { TCreateAccessApprovalPolicy, TDeleteAccessApprovalPolicy, + TGetAccessPolicyCountByEnvironmentDTO, TListAccessApprovalPoliciesDTO, TUpdateAccessApprovalPolicy } from "./access-approval-policy-types"; type TSecretApprovalPolicyServiceFactoryDep = { + projectDAL: TProjectDALFactory; permissionService: Pick; accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory; - projectEnvDAL: Pick; + projectEnvDAL: Pick; accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory; projectMembershipDAL: Pick; }; @@ -30,6 +34,7 @@ export const accessApprovalPolicyServiceFactory = ({ accessApprovalPolicyApproverDAL, permissionService, projectEnvDAL, + projectDAL, projectMembershipDAL }: TSecretApprovalPolicyServiceFactoryDep) => { const createAccessApprovalPolicy = async ({ @@ -37,19 +42,24 @@ export const accessApprovalPolicyServiceFactory = ({ actor, actorId, actorOrgId, + secretPath, actorAuthMethod, approvals, approvers, - projectId, + projectSlug, environment }: TCreateAccessApprovalPolicy) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + if (approvals > approvers.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); + if (!secretPath) throw new BadRequestError({ message: "Secret path is required" }); const { permission } = await permissionService.getProjectPermission( actor, actorId, - projectId, + project.id, actorAuthMethod, actorOrgId ); @@ -57,13 +67,24 @@ export const accessApprovalPolicyServiceFactory = ({ ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval ); - const env = await projectEnvDAL.findOne({ slug: environment, projectId }); + const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id }); if (!env) throw new BadRequestError({ message: "Environment not found" }); const secretApprovers = await projectMembershipDAL.find({ - projectId, + projectId: project.id, $in: { id: approvers } }); + + await verifyApprovers({ + projectId: project.id, + orgId: actorOrgId, + envSlug: environment, + secretPath, + actorAuthMethod, + permissionService, + approverProjectMemberships: secretApprovers + }); + if (secretApprovers.length !== approvers.length) { throw new BadRequestError({ message: "Approver not found in project" }); } @@ -73,6 +94,7 @@ export const accessApprovalPolicyServiceFactory = ({ { envId: env.id, approvals, + secretPath, name }, tx @@ -86,7 +108,7 @@ export const accessApprovalPolicyServiceFactory = ({ ); return doc; }); - return { ...accessApproval, environment: env, projectId }; + return { ...accessApproval, environment: env, projectId: project.id }; }; const getAccessApprovalPolicyByProjectId = async ({ @@ -94,24 +116,29 @@ export const accessApprovalPolicyServiceFactory = ({ actor, actorOrgId, actorAuthMethod, - projectId + projectSlug }: TListAccessApprovalPoliciesDTO) => { - const { permission } = await permissionService.getProjectPermission( + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + + // Anyone in the project should be able to get the policies. + /* const { permission } = */ await permissionService.getProjectPermission( actor, actorId, - projectId, + project.id, actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + // ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); - const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId }); + const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id }); return accessApprovalPolicies; }; const updateAccessApprovalPolicy = async ({ policyId, approvers, + secretPath, name, actorId, actor, @@ -121,7 +148,6 @@ export const accessApprovalPolicyServiceFactory = ({ }: TUpdateAccessApprovalPolicy) => { const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId); if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); - const { permission } = await permissionService.getProjectPermission( actor, actorId, @@ -129,6 +155,7 @@ export const accessApprovalPolicyServiceFactory = ({ actorAuthMethod, actorOrgId ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { @@ -136,6 +163,7 @@ export const accessApprovalPolicyServiceFactory = ({ accessApprovalPolicy.id, { approvals, + secretPath, name }, tx @@ -149,6 +177,17 @@ export const accessApprovalPolicyServiceFactory = ({ }, { tx } ); + + await verifyApprovers({ + projectId: accessApprovalPolicy.projectId, + orgId: actorOrgId, + envSlug: accessApprovalPolicy.environment.slug, + secretPath: doc.secretPath!, + actorAuthMethod, + permissionService, + approverProjectMemberships: secretApprovers + }); + if (secretApprovers.length !== approvers.length) throw new BadRequestError({ message: "Approver not found in project" }); if (doc.approvals > secretApprovers.length) @@ -197,7 +236,31 @@ export const accessApprovalPolicyServiceFactory = ({ return policy; }; + const getAccessPolicyCountByEnvSlug = async ({ + actor, + actorOrgId, + actorAuthMethod, + projectSlug, + actorId, + envSlug + }: TGetAccessPolicyCountByEnvironmentDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + + if (!project) throw new BadRequestError({ message: "Project not found" }); + + await permissionService.getProjectPermission(actor, actorId, project.id, actorAuthMethod, actorOrgId); + + const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); + if (!environment) throw new BadRequestError({ message: "Environment not found" }); + + const policies = await accessApprovalPolicyDAL.find({ envId: environment.id, projectId: project.id }); + if (!policies) throw new BadRequestError({ message: "No policies found" }); + + return { policyCount: policies.length }; + }; + return { + getAccessPolicyCountByEnvSlug, createAccessApprovalPolicy, deleteAccessApprovalPolicy, updateAccessApprovalPolicy, From 317956a03844a91d06b23a82f9b3f95170a10477 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:39:48 -0700 Subject: [PATCH 013/158] Fix: Types mismatch --- .../access-approval-policy-types.ts | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts index 034132d04..c7b452771 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts @@ -1,10 +1,25 @@ +import { TProjectMemberships } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; +import { ActorAuthMethod } from "@app/services/auth/auth-type"; + +import { TPermissionServiceFactory } from "../permission/permission-service"; + +export type TVerifyApprovers = { + approverProjectMemberships: TProjectMemberships[]; + permissionService: Pick; + envSlug: string; + actorAuthMethod: ActorAuthMethod; + secretPath: string; + projectId: string; + orgId: string; +}; export type TCreateAccessApprovalPolicy = { approvals: number; + secretPath?: string | null; environment: string; approvers: string[]; - projectId: string; + projectSlug: string; name: string; } & Omit; @@ -12,6 +27,7 @@ export type TUpdateAccessApprovalPolicy = { policyId: string; approvals?: number; approvers: string[]; + secretPath?: string; name?: string; } & Omit; @@ -19,7 +35,14 @@ export type TDeleteAccessApprovalPolicy = { policyId: string; } & Omit; -export type TListAccessApprovalPoliciesDTO = TProjectPermission; +export type TGetAccessPolicyCountByEnvironmentDTO = { + envSlug: string; + projectSlug: string; +} & Omit; + +export type TListAccessApprovalPoliciesDTO = { + projectSlug: string; +} & Omit; export type TGetBoardAccessApprovalPolicy = { projectId: string; From 992a82015aac946f6882e4313c37d6874b650182 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:40:05 -0700 Subject: [PATCH 014/158] Feat: Request access --- .../access-approval-request-dal.ts | 265 ++++++++++++++++++ .../access-approval-request-reviewer-dal.ts | 10 + .../access-approval-request-secret-dal.ts | 230 +++++++++++++++ 3 files changed, 505 insertions(+) create mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-dal.ts create mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts create mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts new file mode 100644 index 000000000..78fef7c8a --- /dev/null +++ b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts @@ -0,0 +1,265 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { AccessApprovalRequestsSchema, TableName, TAccessApprovalRequests } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex"; + +import { ApprovalStatus } from "./access-approval-request-types"; + +export type TAccessApprovalRequestDALFactory = ReturnType; + +export const accessApprovalRequestDALFactory = (db: TDbClient) => { + const accessApprovalRequestOrm = ormify(db, TableName.AccessApprovalRequest); + + const findRequestsWithPrivilegeByPolicyIds = async (policyIds: string[]) => { + try { + const docs = await db(TableName.AccessApprovalRequest) + .whereIn(`${TableName.AccessApprovalRequest}.policyId`, policyIds) + + .leftJoin( + TableName.ProjectUserAdditionalPrivilege, + `${TableName.AccessApprovalRequest}.privilegeId`, + `${TableName.ProjectUserAdditionalPrivilege}.id` + ) + .leftJoin( + TableName.AccessApprovalPolicy, + `${TableName.AccessApprovalRequest}.policyId`, + `${TableName.AccessApprovalPolicy}.id` + ) + + .leftJoin( + TableName.AccessApprovalRequestReviewer, + `${TableName.AccessApprovalRequest}.id`, + `${TableName.AccessApprovalRequestReviewer}.requestId` + ) + .leftJoin( + TableName.AccessApprovalPolicyApprover, + `${TableName.AccessApprovalPolicy}.id`, + `${TableName.AccessApprovalPolicyApprover}.policyId` + ) + + .leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) + + .select(selectAllTableCols(TableName.AccessApprovalRequest)) + .select( + db.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"), + db.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"), + db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"), + db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"), + db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId") + ) + + .select(db.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover)) + + .select( + db.ref("projectId").withSchema(TableName.Environment), + db.ref("slug").withSchema(TableName.Environment).as("envSlug"), + db.ref("name").withSchema(TableName.Environment).as("envName") + ) + + .select( + db.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"), + db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus") + ) + + .select( + db + .ref("projectMembershipId") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("privilegeMembershipId"), + db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeIsTemporary"), + db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryMode"), + db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("privilegeTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("privilegeTemporaryAccessEndTime"), + + db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegePermissions") + ); + + const formattedDocs = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (doc) => ({ + ...AccessApprovalRequestsSchema.parse(doc), + projectId: doc.projectId, + environment: doc.envSlug, + environmentName: doc.envName, + policy: { + id: doc.policyId, + name: doc.policyName, + approvals: doc.policyApprovals, + secretPath: doc.policySecretPath, + envId: doc.policyEnvId + }, + privilege: doc.privilegeId + ? { + membershipId: doc.privilegeMembershipId, + isTemporary: doc.privilegeIsTemporary, + temporaryMode: doc.privilegeTemporaryMode, + temporaryRange: doc.privilegeTemporaryRange, + temporaryAccessStartTime: doc.privilegeTemporaryAccessStartTime, + temporaryAccessEndTime: doc.privilegeTemporaryAccessEndTime, + permissions: doc.privilegePermissions + } + : null, + + isApproved: !!doc.privilegeId + }), + childrenMapper: [ + { + key: "reviewerMemberId", + label: "reviewers" as const, + mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined) + }, + { key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId } + ] + }); + + if (!formattedDocs) return []; + + return formattedDocs.map((doc) => ({ + ...doc, + policy: { ...doc.policy, approvers: doc.approvers } + })); + } catch (error) { + throw new DatabaseError({ error, name: "FindRequestsWithPrivilege" }); + } + }; + + const findQuery = (filter: TFindFilter, tx: Knex) => + tx(TableName.AccessApprovalRequest) + .where(filter) + .join( + TableName.AccessApprovalPolicy, + `${TableName.AccessApprovalRequest}.policyId`, + `${TableName.AccessApprovalPolicy}.id` + ) + + .join( + TableName.AccessApprovalPolicyApprover, + `${TableName.AccessApprovalPolicy}.id`, + `${TableName.AccessApprovalPolicyApprover}.policyId` + ) + .leftJoin( + TableName.AccessApprovalRequestReviewer, + `${TableName.AccessApprovalRequest}.id`, + `${TableName.AccessApprovalRequestReviewer}.requestId` + ) + + .leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) + .select(selectAllTableCols(TableName.AccessApprovalRequest)) + .select( + tx.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"), + tx.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"), + tx.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"), + tx.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"), + tx.ref("projectId").withSchema(TableName.Environment), + tx.ref("slug").withSchema(TableName.Environment).as("environment"), + tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"), + tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"), + tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover) + ); + + const findById = async (id: string, tx?: Knex) => { + try { + const sql = findQuery({ [`${TableName.AccessApprovalRequest}.id` as "id"]: id }, tx || db); + const docs = await sql; + const formatedDoc = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ + ...AccessApprovalRequestsSchema.parse(el), + projectId: el.projectId, + environment: el.environment, + policy: { + id: el.policyId, + name: el.policyName, + approvals: el.policyApprovals, + secretPath: el.policySecretPath + } + }), + childrenMapper: [ + { + key: "reviewerMemberId", + label: "reviewers" as const, + mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined) + }, + { key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId } + ] + }); + if (!formatedDoc?.[0]) return; + return { + ...formatedDoc[0], + policy: { ...formatedDoc[0].policy, approvers: formatedDoc[0].approvers } + }; + } catch (error) { + throw new DatabaseError({ error, name: "FindByIdAccessApprovalRequest" }); + } + }; + + const getCount = async ({ projectId }: { projectId: string }) => { + try { + const accessRequests = await db(TableName.AccessApprovalRequest) + .leftJoin( + TableName.AccessApprovalPolicy, + `${TableName.AccessApprovalRequest}.policyId`, + `${TableName.AccessApprovalPolicy}.id` + ) + .leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) + .leftJoin( + TableName.ProjectUserAdditionalPrivilege, + `${TableName.AccessApprovalRequest}.privilegeId`, + `${TableName.ProjectUserAdditionalPrivilege}.id` + ) + + .leftJoin( + TableName.AccessApprovalRequestReviewer, + `${TableName.AccessApprovalRequest}.id`, + `${TableName.AccessApprovalRequestReviewer}.requestId` + ) + + .where(`${TableName.Environment}.projectId`, projectId) + .select(selectAllTableCols(TableName.AccessApprovalRequest)) + .select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")) + .select(db.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId")); + + const formattedRequests = sqlNestRelationships({ + data: accessRequests, + key: "id", + parentMapper: (doc) => ({ + ...AccessApprovalRequestsSchema.parse(doc) + }), + childrenMapper: [ + { + key: "reviewerMemberId", + label: "reviewers" as const, + mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined) + } + ] + }); + + // an approval is pending if there is no reviewer rejections and no privilege ID is set + const pendingApprovals = formattedRequests.filter( + (req) => !req.privilegeId && !req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) + ); + + // an approval is finalized if there are any rejections or a privilege ID is set + const finalizedApprovals = formattedRequests.filter( + (req) => req.privilegeId || req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) + ); + + return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length }; + } catch (error) { + throw new DatabaseError({ error, name: "GetCountAccessApprovalRequest" }); + } + }; + + return { ...accessApprovalRequestOrm, findById, findRequestsWithPrivilegeByPolicyIds, getCount }; +}; diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts new file mode 100644 index 000000000..251015b22 --- /dev/null +++ b/backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TAccessApprovalRequestReviewerDALFactory = ReturnType; + +export const accessApprovalRequestReviewerDALFactory = (db: TDbClient) => { + const secretApprovalRequestReviewerOrm = ormify(db, TableName.AccessApprovalRequestReviewer); + return secretApprovalRequestReviewerOrm; +}; diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts new file mode 100644 index 000000000..d458d58ff --- /dev/null +++ b/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts @@ -0,0 +1,230 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { + SecretApprovalRequestsSecretsSchema, + TableName, + TSecretApprovalRequestsSecrets, + TSecretTags +} from "@app/db/schemas"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; + +export type TAccessApprovalRequestSecretDALFactory = ReturnType; + +export const accessApprovalRequestSecretDALFactory = (db: TDbClient) => { + const accessApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret); + const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag); + + const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => { + try { + const existingApprovalSecrets = await accessApprovalRequestSecretOrm.find( + { + $in: { + id: data.map((el) => el.id) + } + }, + { tx } + ); + + if (existingApprovalSecrets.length !== data.length) { + throw new BadRequestError({ message: "Some of the secret approvals do not exist" }); + } + + if (data.length === 0) return []; + + const updatedApprovalSecrets = await (tx || db)(TableName.SecretApprovalRequestSecret) + .insert(data) + .onConflict("id") // this will cause a conflict then merge the data + .merge() // Merge the data with the existing data + .returning("*"); + + return updatedApprovalSecrets; + } catch (error) { + throw new DatabaseError({ error, name: "bulk update secret" }); + } + }; + + const findByRequestId = async (requestId: string, tx?: Knex) => { + try { + const doc = await (tx || db)({ + secVerTag: TableName.SecretTag + }) + .from(TableName.SecretApprovalRequestSecret) + .where({ requestId }) + .leftJoin( + TableName.SecretApprovalRequestSecretTag, + `${TableName.SecretApprovalRequestSecret}.id`, + `${TableName.SecretApprovalRequestSecretTag}.secretId` + ) + .leftJoin(TableName.SecretTag, `${TableName.SecretApprovalRequestSecretTag}.tagId`, `${TableName.SecretTag}.id`) + .leftJoin(TableName.Secret, `${TableName.SecretApprovalRequestSecret}.secretId`, `${TableName.Secret}.id`) + .leftJoin( + TableName.SecretVersion, + `${TableName.SecretVersion}.id`, + `${TableName.SecretApprovalRequestSecret}.secretVersion` + ) + .leftJoin( + TableName.SecretVersionTag, + `${TableName.SecretVersionTag}.${TableName.SecretVersion}Id`, + `${TableName.SecretVersion}.id` + ) + .leftJoin( + db.ref(TableName.SecretTag).as("secVerTag"), + `${TableName.SecretVersionTag}.${TableName.SecretTag}Id`, + db.ref("id").withSchema("secVerTag") + ) + .select(selectAllTableCols(TableName.SecretApprovalRequestSecret)) + .select({ + secVerTagId: "secVerTag.id", + secVerTagColor: "secVerTag.color", + secVerTagSlug: "secVerTag.slug", + secVerTagName: "secVerTag.name" + }) + .select( + db.ref("id").withSchema(TableName.SecretTag).as("tagId"), + db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"), + db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), + db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), + db.ref("name").withSchema(TableName.SecretTag).as("tagName") + ) + .select( + db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"), + db.ref("version").withSchema(TableName.Secret).as("orgSecVersion"), + db.ref("secretKeyIV").withSchema(TableName.Secret).as("orgSecKeyIV"), + db.ref("secretKeyTag").withSchema(TableName.Secret).as("orgSecKeyTag"), + db.ref("secretKeyCiphertext").withSchema(TableName.Secret).as("orgSecKeyCiphertext"), + db.ref("secretValueIV").withSchema(TableName.Secret).as("orgSecValueIV"), + db.ref("secretValueTag").withSchema(TableName.Secret).as("orgSecValueTag"), + db.ref("secretValueCiphertext").withSchema(TableName.Secret).as("orgSecValueCiphertext"), + db.ref("secretCommentIV").withSchema(TableName.Secret).as("orgSecCommentIV"), + db.ref("secretCommentTag").withSchema(TableName.Secret).as("orgSecCommentTag"), + db.ref("secretCommentCiphertext").withSchema(TableName.Secret).as("orgSecCommentCiphertext") + ) + .select( + db.ref("version").withSchema(TableName.SecretVersion).as("secVerVersion"), + db.ref("secretKeyIV").withSchema(TableName.SecretVersion).as("secVerKeyIV"), + db.ref("secretKeyTag").withSchema(TableName.SecretVersion).as("secVerKeyTag"), + db.ref("secretKeyCiphertext").withSchema(TableName.SecretVersion).as("secVerKeyCiphertext"), + db.ref("secretValueIV").withSchema(TableName.SecretVersion).as("secVerValueIV"), + db.ref("secretValueTag").withSchema(TableName.SecretVersion).as("secVerValueTag"), + db.ref("secretValueCiphertext").withSchema(TableName.SecretVersion).as("secVerValueCiphertext"), + db.ref("secretCommentIV").withSchema(TableName.SecretVersion).as("secVerCommentIV"), + db.ref("secretCommentTag").withSchema(TableName.SecretVersion).as("secVerCommentTag"), + db.ref("secretCommentCiphertext").withSchema(TableName.SecretVersion).as("secVerCommentCiphertext") + ); + const formatedDoc = sqlNestRelationships({ + data: doc, + key: "id", + parentMapper: (data) => SecretApprovalRequestsSecretsSchema.omit({ secretVersion: true }).parse(data), + childrenMapper: [ + { + key: "tagJnId", + label: "tags" as const, + mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({ + id, + name, + slug, + color + }) + }, + { + key: "secretId", + label: "secret" as const, + mapper: ({ + orgSecKeyIV, + orgSecKeyTag, + orgSecValueIV, + orgSecVersion, + orgSecValueTag, + orgSecCommentIV, + orgSecBlindIndex, + orgSecCommentTag, + orgSecKeyCiphertext, + orgSecValueCiphertext, + orgSecCommentCiphertext, + secretId + }) => + secretId + ? { + id: secretId, + version: orgSecVersion, + secretBlindIndex: orgSecBlindIndex, + secretKeyIV: orgSecKeyIV, + secretKeyTag: orgSecKeyTag, + secretKeyCiphertext: orgSecKeyCiphertext, + secretValueIV: orgSecValueIV, + secretValueTag: orgSecValueTag, + secretValueCiphertext: orgSecValueCiphertext, + secretCommentIV: orgSecCommentIV, + secretCommentTag: orgSecCommentTag, + secretCommentCiphertext: orgSecCommentCiphertext + } + : undefined + }, + { + key: "secretVersion", + label: "secretVersion" as const, + mapper: ({ + secVerCommentIV, + secVerCommentCiphertext, + secVerCommentTag, + secVerValueCiphertext, + secVerKeyIV, + secVerKeyTag, + secVerValueIV, + secretVersion, + secVerValueTag, + secVerKeyCiphertext, + secVerVersion + }) => + secretVersion + ? { + version: secVerVersion, + id: secretVersion, + secretKeyIV: secVerKeyIV, + secretKeyTag: secVerKeyTag, + secretKeyCiphertext: secVerKeyCiphertext, + secretValueIV: secVerValueIV, + secretValueTag: secVerValueTag, + secretValueCiphertext: secVerValueCiphertext, + secretCommentIV: secVerCommentIV, + secretCommentTag: secVerCommentTag, + secretCommentCiphertext: secVerCommentCiphertext + } + : undefined, + childrenMapper: [ + { + key: "secVerTagId", + label: "tags" as const, + mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({ + // eslint-disable-next-line + id, + // eslint-disable-next-line + name, + // eslint-disable-next-line + slug, + // eslint-disable-next-line + color + }) + } + ] + } + ] + }); + return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({ + ...el, + secret: secret?.[0], + secretVersion: secretVersion?.[0] + })); + } catch (error) { + throw new DatabaseError({ error, name: "FindByRequestId" }); + } + }; + return { + ...accessApprovalRequestSecretOrm, + findByRequestId, + bulkUpdateNoVersionIncrement, + insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany + }; +}; From ad405109a0526c4ea20ca127b0d452500d3f04a8 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:40:51 -0700 Subject: [PATCH 015/158] Feat: Request access --- .../access-approval-request-service.ts | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-service.ts diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts new file mode 100644 index 000000000..d02842939 --- /dev/null +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -0,0 +1,269 @@ +import { ForbiddenError } from "@casl/ability"; +import ms from "ms"; + +import { ProjectMembershipRole } from "@app/db/schemas"; +import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; +import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; + +import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; +import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns"; +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; +import { TProjectUserAdditionalPrivilegeServiceFactory } from "../project-user-additional-privilege/project-user-additional-privilege-service"; +import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types"; +import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal"; +import { TAccessApprovalRequestReviewerDALFactory } from "./access-approval-request-reviewer-dal"; +import { + ApprovalStatus, + TCreateAccessApprovalRequestDTO, + TGetAccessRequestCountDTO, + TListApprovalRequestsDTO, + TReviewAccessRequestDTO +} from "./access-approval-request-types"; + +type TSecretApprovalRequestServiceFactoryDep = { + additionalPrivilegeService: TProjectUserAdditionalPrivilegeServiceFactory; + additionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory; + permissionService: TPermissionServiceFactory; + projectEnvDAL: Pick; + projectDAL: Pick; + accessApprovalRequestDAL: TAccessApprovalRequestDALFactory; + accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory; + accessApprovalRequestReviewerDAL: TAccessApprovalRequestReviewerDALFactory; + projectMembershipDAL: TProjectMembershipDALFactory; +}; + +export type TAccessApprovalRequestServiceFactory = ReturnType; + +export const accessApprovalRequestServiceFactory = ({ + projectDAL, + projectEnvDAL, + permissionService, + accessApprovalRequestDAL, + accessApprovalRequestReviewerDAL, + projectMembershipDAL, + accessApprovalPolicyDAL, + additionalPrivilegeDAL +}: TSecretApprovalRequestServiceFactoryDep) => { + const createAccessApprovalRequest = async ({ + isTemporary, + temporaryRange, + actorId, + envSlug, + permissions, + secretPath, + actor, + actorOrgId, + actorAuthMethod, + projectSlug + }: TCreateAccessApprovalRequestDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new UnauthorizedError({ message: "Project not found" }); + + // Anyone can create an access approval request. + const p = await permissionService.getProjectPermission(actor, actorId, project.id, actorAuthMethod, actorOrgId); + + if (!p.membership) throw new UnauthorizedError({ message: "You are not a member of this project" }); + + await projectDAL.checkProjectUpgradeStatus(project.id); + + const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); + + if (!environment) throw new UnauthorizedError({ message: "Environment not found" }); + if (!secretPath) throw new UnauthorizedError({ message: "Secret path is required" }); + + const policy = await accessApprovalPolicyDAL.findOne({ + envId: environment.id, + secretPath + }); + if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." }); + + const approvalRequest = await accessApprovalRequestDAL.create({ + policyId: policy.id, + requestedBy: p.membership.id, + temporaryRange: temporaryRange || null, + permissions: JSON.stringify(permissions), + isTemporary + }); + + return { request: approvalRequest }; + }; + + const listApprovalRequests = async ({ + projectSlug, + authorProjectMembershipId, + envSlug, + actor, + actorOrgId, + actorId, + actorAuthMethod + }: TListApprovalRequestsDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new UnauthorizedError({ message: "Project not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + project.id, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + + const policies = await accessApprovalPolicyDAL.find({ projectId: project.id }); + let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id)); + + if (authorProjectMembershipId) { + requests = requests.filter((request) => request.requestedBy === authorProjectMembershipId); + } + + if (envSlug) { + requests = requests.filter((request) => request.environment === envSlug); + } + + return { requests }; + }; + + const reviewAccessRequest = async ({ + requestId, + actor, + status, + actorId, + actorAuthMethod, + actorOrgId + }: TReviewAccessRequestDTO) => { + const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId); + if (!accessApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); + + const { policy } = accessApprovalRequest; + const { membership, hasRole } = await permissionService.getProjectPermission( + actor, + actorId, + accessApprovalRequest.projectId, + actorAuthMethod, + actorOrgId + ); + + if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" }); + + if ( + !hasRole(ProjectMembershipRole.Admin) && + accessApprovalRequest.requestedBy !== membership.id && // The request wasn't made by the current user + !policy.approvers.find((approverId) => approverId === membership.id) // The request isn't performed by an assigned approver + ) { + throw new UnauthorizedError({ message: "No access" }); + } + + const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id); + + await verifyApprovers({ + projectId: accessApprovalRequest.projectId, + orgId: actorOrgId, + envSlug: accessApprovalRequest.environment, + secretPath: accessApprovalRequest.policy.secretPath!, + actorAuthMethod, + permissionService, + approverProjectMemberships: [reviewerProjectMembership] + }); + + const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id }); + if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) { + throw new BadRequestError({ message: "The request has already been rejected by another reviewer" }); + } + + const reviewStatus = await accessApprovalRequestReviewerDAL.transaction(async (tx) => { + const review = await accessApprovalRequestReviewerDAL.findOne( + { + requestId: accessApprovalRequest.id, + member: membership.id + }, + tx + ); + if (!review) { + const newReview = await accessApprovalRequestReviewerDAL.create( + { + status, + requestId: accessApprovalRequest.id, + member: membership.id + }, + tx + ); + + const allReviews = [...existingReviews, newReview]; + + const approvedReviews = allReviews.filter((r) => r.status === ApprovalStatus.APPROVED); + + // If all approvers have approved the request, update the privilege to approved + if (approvedReviews.length === policy.approvers.length) { + if (accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) { + throw new BadRequestError({ message: "Temporary range is required for temporary access" }); + } + + let privilegeId: string | null = null; + + if (!accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) { + // Permanent access + const privilege = await additionalPrivilegeDAL.create({ + projectMembershipId: accessApprovalRequest.requestedBy, + slug: "", + permissions: JSON.stringify(accessApprovalRequest.permissions) + }); + privilegeId = privilege.id; + } else { + // Temporary access + const relativeTempAllocatedTimeInMs = ms(accessApprovalRequest.temporaryRange!); + const startTime = new Date(); + + const privilege = await additionalPrivilegeDAL.create({ + projectMembershipId: accessApprovalRequest.requestedBy, + slug: "", + permissions: JSON.stringify(accessApprovalRequest.permissions), + isTemporary: true, + temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, + temporaryRange: accessApprovalRequest.temporaryRange!, + temporaryAccessStartTime: startTime, + temporaryAccessEndTime: new Date(new Date(startTime).getTime() + relativeTempAllocatedTimeInMs) + }); + privilegeId = privilege.id; + } + + await accessApprovalRequestDAL.updateById(accessApprovalRequest.id, { privilegeId }, tx); + } + + return newReview; + } + throw new BadRequestError({ message: "You have already reviewed this request" }); + }); + + return reviewStatus; + }; + + const getCount = async ({ projectSlug, actor, actorAuthMethod, actorId, actorOrgId }: TGetAccessRequestCountDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new UnauthorizedError({ message: "Project not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + project.id, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + + const count = await accessApprovalRequestDAL.getCount({ projectId: project.id }); + + return { count }; + }; + + return { + createAccessApprovalRequest, + listApprovalRequests, + reviewAccessRequest, + getCount + }; +}; From ad6e2aeb9e7ca01403bfaa8098006d5280fa561f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:43:05 -0700 Subject: [PATCH 016/158] Feat: Request Access --- .../access-approval-request-types.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-types.ts diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts new file mode 100644 index 000000000..6bba0c14d --- /dev/null +++ b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts @@ -0,0 +1,31 @@ +import { TProjectPermission } from "@app/lib/types"; + +export enum ApprovalStatus { + PENDING = "pending", + APPROVED = "approved", + REJECTED = "rejected" +} + +export type TGetAccessRequestCountDTO = { + projectSlug: string; +} & Omit; + +export type TReviewAccessRequestDTO = { + requestId: string; + status: ApprovalStatus; +} & Omit; + +export type TCreateAccessApprovalRequestDTO = { + projectSlug: string; + secretPath: string; + envSlug: string; + permissions: unknown; + isTemporary: boolean; + temporaryRange?: string; +} & Omit; + +export type TListApprovalRequestsDTO = { + projectSlug: string; + authorProjectMembershipId?: string; + envSlug?: string; +} & Omit; From 696479a2efb098b27f694e349c84449eb3da7ed2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:43:48 -0700 Subject: [PATCH 017/158] Fix: Remove redundant code --- .../src/components/permissions/PermissionDeniedBanner.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx index f9707d634..b3c7a4f53 100644 --- a/frontend/src/components/permissions/PermissionDeniedBanner.tsx +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -3,8 +3,6 @@ import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { Button } from "../v2"; - type Props = { containerClassName?: string; className?: string; @@ -34,9 +32,6 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children )}
- ); From b298eec9dba82f77a0f27d3153bb526808d9c91e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:44:00 -0700 Subject: [PATCH 018/158] Fix: Danger color not working on disabled buttons --- frontend/src/components/v2/Button/Button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/v2/Button/Button.tsx b/frontend/src/components/v2/Button/Button.tsx index 5536d699a..7707805e9 100644 --- a/frontend/src/components/v2/Button/Button.tsx +++ b/frontend/src/components/v2/Button/Button.tsx @@ -29,7 +29,7 @@ const buttonVariants = cva( colorSchema: { primary: ["bg-primary", "text-black", "border-primary bg-opacity-90 hover:bg-opacity-100"], secondary: ["bg-mineshaft", "text-gray-300", "border-mineshaft hover:bg-opacity-80"], - danger: ["bg-red", "text-white", "border-red hover:bg-opacity-90"], + danger: ["!bg-red", "!text-white", "!border-red hover:!bg-opacity-90"], gray: ["bg-bunker-500", "text-bunker-200"] }, variant: { From 7fd1d729850da8dc177fcaff652ab18d6c62c274 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:44:25 -0700 Subject: [PATCH 019/158] Fix: Access Request setup --- backend/src/server/routes/index.ts | 35 +++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 0a2c77c9f..905d4028c 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -5,6 +5,9 @@ import { registerV1EERoutes } from "@app/ee/routes/v1"; import { accessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal"; import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal"; import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service"; +import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal"; +import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal"; +import { accessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service"; import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal"; import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue"; import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; @@ -212,7 +215,9 @@ export const registerRoutes = async ( const ldapGroupMapDAL = ldapGroupMapDALFactory(db); const accessApprovalPolicyDAL = accessApprovalPolicyDALFactory(db); + const accessApprovalRequestDAL = accessApprovalRequestDALFactory(db); const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db); + const accessApprovalRequestReviewerDAL = accessApprovalRequestReviewerDALFactory(db); const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db); const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db); @@ -273,14 +278,6 @@ export const registerRoutes = async ( }); const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL }); - const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({ - accessApprovalPolicyDAL, - accessApprovalPolicyApproverDAL, - permissionService, - projectEnvDAL, - projectMembershipDAL - }); - const samlService = samlConfigServiceFactory({ permissionService, orgBotDAL, @@ -613,6 +610,27 @@ export const registerRoutes = async ( secretQueueService }); + const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({ + accessApprovalPolicyDAL, + accessApprovalPolicyApproverDAL, + permissionService, + projectEnvDAL, + projectMembershipDAL, + projectDAL + }); + + const accessApprovalRequestService = accessApprovalRequestServiceFactory({ + projectDAL, + permissionService, + accessApprovalRequestReviewerDAL, + additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL, + projectMembershipDAL, + additionalPrivilegeService: projectUserAdditionalPrivilegeService, + accessApprovalPolicyDAL, + accessApprovalRequestDAL, + projectEnvDAL + }); + const secretRotationQueue = secretRotationQueueFactory({ telemetryService, secretRotationDAL, @@ -750,6 +768,7 @@ export const registerRoutes = async ( identityUa: identityUaService, secretApprovalPolicy: sapService, accessApprovalPolicy: accessApprovalPolicyService, + accessApprovalRequest: accessApprovalRequestService, secretApprovalRequest: sarService, secretRotation: secretRotationService, dynamicSecret: dynamicSecretService, From 1f68730aa3aedc49acfe06f06f6ad6cc685093c5 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:44:49 -0700 Subject: [PATCH 020/158] Fix: Improve disabled Select --- frontend/src/components/v2/Select/Select.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx index 2a76be2ab..12a9094e0 100644 --- a/frontend/src/components/v2/Select/Select.tsx +++ b/frontend/src/components/v2/Select/Select.tsx @@ -41,18 +41,22 @@ export const Select = forwardRef( ref={ref} className={twMerge( `inline-flex items-center justify-between rounded-md - bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200 focus:bg-mineshaft-700/80`, - className + bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none focus:bg-mineshaft-700/80 data-[placeholder]:text-mineshaft-200`, + className, + isDisabled && "cursor-not-allowed opacity-50" )} > {props.icon ? : placeholder} - {!isDisabled && ( - - - - )} + + + + Date: Wed, 3 Apr 2024 16:45:18 -0700 Subject: [PATCH 021/158] Update index.tsx --- frontend/src/hooks/api/accessApproval/index.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/hooks/api/accessApproval/index.tsx b/frontend/src/hooks/api/accessApproval/index.tsx index 537dfe3be..ec9789d05 100644 --- a/frontend/src/hooks/api/accessApproval/index.tsx +++ b/frontend/src/hooks/api/accessApproval/index.tsx @@ -1,6 +1,13 @@ export { useCreateAccessApprovalPolicy, + useCreateAccessRequest, useDeleteAccessApprovalPolicy, + useReviewAccessRequest, useUpdateAccessApprovalPolicy } from "./mutation"; -export { useGetAccessApprovalPolicies } from "./queries"; +export { + useGetAccessApprovalPolicies, + useGetAccessApprovalRequests, + useGetAccessPolicyApprovalCount, + useGetAccessRequestsCount +} from "./queries"; From 7aee4fdfcdfdf3f312504e6e62841b6cc07a97e6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:45:37 -0700 Subject: [PATCH 022/158] Feat: Request access --- .../src/hooks/api/accessApproval/mutation.tsx | 87 ++++++++-- .../src/hooks/api/accessApproval/queries.tsx | 149 ++++++++++++++++-- .../src/hooks/api/accessApproval/types.ts | 106 ++++++++++++- 3 files changed, 312 insertions(+), 30 deletions(-) diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index 98e9b3f97..7a150092c 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -1,26 +1,34 @@ +import { packRules } from "@casl/ability/extra"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { accessApprovalKeys } from "./queries"; -import { TCreateAccessPolicyDTO, TDeleteSecretPolicyDTO, TUpdateAccessPolicyDTO } from "./types"; +import { + TAccessApproval, + TCreateAccessPolicyDTO, + TCreateAccessRequestDTO, + TDeleteSecretPolicyDTO, + TUpdateAccessPolicyDTO +} from "./types"; export const useCreateAccessApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TCreateAccessPolicyDTO>({ - mutationFn: async ({ environment, workspaceId, approvals, approvers, name }) => { + mutationFn: async ({ environment, projectSlug, approvals, approvers, name, secretPath }) => { const { data } = await apiRequest.post("/api/v1/access-approvals", { environment, - workspaceId, + projectSlug, approvals, approvers, + secretPath, name }); return data; }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(workspaceId)); + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug)); } }); }; @@ -29,16 +37,17 @@ export const useUpdateAccessApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TUpdateAccessPolicyDTO>({ - mutationFn: async ({ id, approvers, approvals, name }) => { + mutationFn: async ({ id, approvers, approvals, name, secretPath }) => { const { data } = await apiRequest.patch(`/api/v1/access-approvals/${id}`, { approvals, approvers, + secretPath, name }); return data; }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(workspaceId)); + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug)); } }); }; @@ -51,8 +60,66 @@ export const useDeleteAccessApprovalPolicy = () => { const { data } = await apiRequest.delete(`/api/v1/access-approvals/${id}`); return data; }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(workspaceId)); + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug)); + } + }); +}; + +export const useCreateAccessRequest = () => { + const queryClient = useQueryClient(); + return useMutation<{}, {}, TCreateAccessRequestDTO>({ + mutationFn: async ({ envSlug, projectSlug, secretPath, ...privilege }) => { + const { data } = await apiRequest.post( + "/api/v1/access-approval-requests", + { + ...privilege, + permissions: privilege.permissions ? packRules(privilege.permissions) : undefined + }, + { + params: { + envSlug, + projectSlug, + secretPath + } + } + ); + + return data; + }, + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequests(projectSlug)); + } + }); +}; + +export const useReviewAccessRequest = () => { + const queryClient = useQueryClient(); + return useMutation< + {}, + {}, + { + requestId: string; + status: "approved" | "rejected"; + projectSlug: string; + envSlug?: string; + requestedBy?: string; + } + >({ + mutationFn: async ({ requestId, status }) => { + const { data } = await apiRequest.post( + `/api/v1/access-approval-requests/${requestId}/review`, + { + status + } + ); + return data; + }, + onSuccess: (_, { projectSlug, envSlug, requestedBy }) => { + queryClient.invalidateQueries( + accessApprovalKeys.getAccessApprovalRequests(projectSlug, envSlug, requestedBy) + ); + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequestCount(projectSlug)); } }); }; diff --git a/frontend/src/hooks/api/accessApproval/queries.tsx b/frontend/src/hooks/api/accessApproval/queries.tsx index 864122593..112c53fe3 100644 --- a/frontend/src/hooks/api/accessApproval/queries.tsx +++ b/frontend/src/hooks/api/accessApproval/queries.tsx @@ -1,30 +1,125 @@ +import { PackRule, unpackRules } from "@casl/ability/extra"; import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { TAccessApprovalPolicy, TGetSecretApprovalPoliciesDTO } from "./types"; +import { TProjectPermission } from "../roles/types"; +import { + TAccessApprovalPolicy, + TAccessApprovalRequest, + TAccessRequestCount, + TGetAccessApprovalRequestsDTO, + TGetAccessPolicyApprovalCountDTO +} from "./types"; export const accessApprovalKeys = { - getAccessApprovalPolicies: (workspaceId: string) => - [{ workspaceId }, "access-approval-policies"] as const, - getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) => [ - { workspaceId, environment }, - "access-approval-policy" - ] + getAccessApprovalPolicies: (projectSlug: string) => + [{ projectSlug }, "access-approval-policies"] as const, + getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) => + [{ workspaceId, environment }, "access-approval-policy"] as const, + + getAccessApprovalRequests: (projectSlug: string, envSlug?: string, requestedBy?: string) => + [{ projectSlug, envSlug, requestedBy }, "access-approval-requests"] as const, + getAccessApprovalRequestCount: (projectSlug: string) => + [{ projectSlug }, "access-approval-request-count"] as const }; -const fetchApprovalPolicies = async (workspaceId: string) => { +export const fetchPolicyApprovalCount = async ({ + projectSlug, + envSlug +}: TGetAccessPolicyApprovalCountDTO) => { + const { data } = await apiRequest.get<{ policyCount: number }>( + "/api/v1/access-approvals/policy-count", + { + params: { projectSlug, envSlug } + } + ); + return data.policyCount; +}; + +export const useGetAccessPolicyApprovalCount = ({ + projectSlug, + envSlug, + options = {} +}: TGetAccessPolicyApprovalCountDTO & { + options?: UseQueryOptions< + number, + unknown, + number, + ReturnType + >; +}) => + useQuery({ + queryFn: () => fetchPolicyApprovalCount({ projectSlug, envSlug }), + ...options, + enabled: Boolean(projectSlug) && (options?.enabled ?? true) + }); + +const fetchApprovalPolicies = async ({ projectSlug }: TGetAccessApprovalRequestsDTO) => { const { data } = await apiRequest.get<{ approvals: TAccessApprovalPolicy[] }>( "/api/v1/access-approvals", - { params: { workspaceId } } + { params: { projectSlug } } ); return data.approvals; }; -export const useGetAccessApprovalPolicies = ({ - workspaceId, +const fetchApprovalRequests = async ({ + projectSlug, + envSlug, + authorProjectMembershipId +}: TGetAccessApprovalRequestsDTO) => { + const { data } = await apiRequest.get<{ requests: TAccessApprovalRequest[] }>( + "/api/v1/access-approval-requests", + { params: { projectSlug, envSlug, authorProjectMembershipId } } + ); + + return data.requests.map((request) => ({ + ...request, + + privilege: request.privilege + ? { + ...request.privilege, + permissions: unpackRules( + request.privilege.permissions as unknown as PackRule[] + ) + } + : null, + permissions: unpackRules(request.permissions as unknown as PackRule[]) + })); +}; + +const fetchAccessRequestsCount = async (projectSlug: string) => { + const { data } = await apiRequest.get( + "/api/v1/access-approval-requests/count", + { params: { projectSlug } } + ); + return data; +}; + +export const useGetAccessRequestsCount = ({ + projectSlug, options = {} -}: TGetSecretApprovalPoliciesDTO & { +}: TGetAccessApprovalRequestsDTO & { + options?: UseQueryOptions< + TAccessRequestCount, + unknown, + { pendingCount: number; finalizedCount: number }, + ReturnType + >; +}) => + useQuery({ + queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug), + queryFn: () => fetchAccessRequestsCount(projectSlug), + ...options, + enabled: Boolean(projectSlug) && (options?.enabled ?? true) + }); + +export const useGetAccessApprovalPolicies = ({ + projectSlug, + envSlug, + authorProjectMembershipId, + options = {} +}: TGetAccessApprovalRequestsDTO & { options?: UseQueryOptions< TAccessApprovalPolicy[], unknown, @@ -33,8 +128,32 @@ export const useGetAccessApprovalPolicies = ({ >; }) => useQuery({ - queryKey: accessApprovalKeys.getAccessApprovalPolicies(workspaceId), - queryFn: () => fetchApprovalPolicies(workspaceId), + queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug), + queryFn: () => fetchApprovalPolicies({ projectSlug, envSlug, authorProjectMembershipId }), ...options, - enabled: Boolean(workspaceId) && (options?.enabled ?? true) + enabled: Boolean(projectSlug) && (options?.enabled ?? true) + }); + +export const useGetAccessApprovalRequests = ({ + projectSlug, + envSlug, + authorProjectMembershipId, + options = {} +}: TGetAccessApprovalRequestsDTO & { + options?: UseQueryOptions< + TAccessApprovalRequest[], + unknown, + TAccessApprovalRequest[], + ReturnType + >; +}) => + useQuery({ + queryKey: accessApprovalKeys.getAccessApprovalRequests( + projectSlug, + envSlug, + authorProjectMembershipId + ), + queryFn: () => fetchApprovalRequests({ projectSlug, envSlug, authorProjectMembershipId }), + ...options, + enabled: Boolean(projectSlug) && (options?.enabled ?? true) }); diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts index 63020166b..1ffd0bf2e 100644 --- a/frontend/src/hooks/api/accessApproval/types.ts +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -1,9 +1,11 @@ +import { TProjectPermission } from "../roles/types"; import { WorkspaceEnv } from "../workspace/types"; export type TAccessApprovalPolicy = { id: string; name: string; approvals: number; + secretPath: string; envId: string; workspace: string; environment: WorkspaceEnv; @@ -11,8 +13,99 @@ export type TAccessApprovalPolicy = { approvers: string[]; }; -export type TGetSecretApprovalPoliciesDTO = { - workspaceId: string; +export type TAccessApprovalRequest = { + id: string; + policyId: string; + privilegeId: string | null; + requestedBy: string; + createdAt: Date; + updatedAt: Date; + isTemporary: boolean; + temporaryRange: string | null | undefined; + + permissions: TProjectPermission[] | null; + + // Computed + environmentName: string; + isApproved: boolean; + + privilege: { + membershipId: string; + isTemporary: boolean; + temporaryMode?: string | null; + temporaryRange?: string | null; + temporaryAccessStartTime?: Date | null; + temporaryAccessEndTime?: Date | null; + permissions: TProjectPermission[]; + isApproved: boolean; + } | null; + + policy: { + id: string; + name: string; + approvals: number; + approvers: string[]; + secretPath?: string | null; + envId: string; + }; + + reviewers: { + member: string; + status: string; + }[]; +}; + +export type TAccessApproval = { + id: string; + policyId: string; + privilegeId: string; + requestedBy: string; +}; + +export type TAccessRequestCount = { + pendingCount: number; + finalizedCount: number; +}; + +export type TProjectUserPrivilege = { + projectMembershipId: string; + slug: string; + id: string; + createdAt: Date; + updatedAt: Date; + permissions?: TProjectPermission[]; +} & ( + | { + isTemporary: true; + temporaryMode: string; + temporaryRange: string; + temporaryAccessStartTime: string; + temporaryAccessEndTime?: string; + } + | { + isTemporary: false; + temporaryMode?: null; + temporaryRange?: null; + temporaryAccessStartTime?: null; + temporaryAccessEndTime?: null; + } +); + +export type TCreateAccessRequestDTO = { + envSlug: string; + projectSlug: string; + secretPath: string; +} & Omit; + +export type TGetAccessApprovalRequestsDTO = { + projectSlug: string; + envSlug?: string; + authorProjectMembershipId?: string; +}; + +export type TGetAccessPolicyApprovalCountDTO = { + projectSlug: string; + envSlug: string; }; export type TGetSecretApprovalPolicyOfBoardDTO = { @@ -22,24 +115,27 @@ export type TGetSecretApprovalPolicyOfBoardDTO = { }; export type TCreateAccessPolicyDTO = { - workspaceId: string; + projectSlug: string; name?: string; environment: string; approvers?: string[]; approvals?: number; + secretPath?: string; }; export type TUpdateAccessPolicyDTO = { id: string; name?: string; approvers?: string[]; + secretPath?: string; + environment?: string; approvals?: number; // for invalidating list - workspaceId: string; + projectSlug: string; }; export type TDeleteSecretPolicyDTO = { id: string; // for invalidating list - workspaceId: string; + projectSlug: string; }; From d380b7f788f150ee0c3ddaeb9db9fc228c604d2a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:49:00 -0700 Subject: [PATCH 023/158] Fix: Added support for request access --- .infisicalignore | 1 + .../SpecificPrivilegeSection.tsx | 270 +++++++++++++++--- 2 files changed, 235 insertions(+), 36 deletions(-) diff --git a/.infisicalignore b/.infisicalignore index d5cc9f15d..855047fe4 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -4,3 +4,4 @@ frontend/src/views/Project/MembersPage/components/IdentityTab/components/Identit frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx:generic-api-key:206 frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292 docs/self-hosting/configuration/envars.mdx:generic-api-key:106 +frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451 diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx index 228683726..89058b08d 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -1,9 +1,11 @@ +import { useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; import { faArrowRotateLeft, faCaretDown, faCheck, faClock, + faLockOpen, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; @@ -44,11 +46,13 @@ import { import { usePopUp } from "@app/hooks"; import { TProjectUserPrivilege, + useCreateAccessRequest, useCreateProjectUserAdditionalPrivilege, useDeleteProjectUserAdditionalPrivilege, useListProjectUserPrivileges, useUpdateProjectUserAdditionalPrivilege } from "@app/hooks/api"; +import { TAccessApprovalPolicy } from "@app/hooks/api/types"; const secretPermissionSchema = z.object({ secretPath: z.string().optional(), @@ -70,51 +74,107 @@ const secretPermissionSchema = z.object({ ]) }); type TSecretPermissionForm = z.infer; -const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPrivilege }) => { +export const SpecificPrivilegeSecretForm = ({ + privilege, + policies, + onClose +}: { + privilege?: TProjectUserPrivilege; + policies?: TAccessApprovalPolicy[]; + onClose?: () => void; +}) => { const { currentWorkspace } = useWorkspace(); + const { membership: projectMembership } = useProjectPermission(); + const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ - "deletePrivilege" + "deletePrivilege", + "requestAccess" ] as const); const { permission } = useProjectPermission(); - const isMemberEditDisabled = permission.cannot( - ProjectPermissionActions.Edit, - ProjectPermissionSub.Member - ); + const isMemberEditDisabled = + permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Member) && !!privilege; const updateUserPrivilege = useUpdateProjectUserAdditionalPrivilege(); const deleteUserPrivilege = useDeleteProjectUserAdditionalPrivilege(); + const requestAccess = useCreateAccessRequest(); const privilegeForm = useForm({ resolver: zodResolver(secretPermissionSchema), values: { - environmentSlug: privilege.permissions?.[0]?.conditions?.environment, - // secret path will be inside $glob operator - secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob || "", - read: privilege.permissions?.some(({ action }) => - action.includes(ProjectPermissionActions.Read) - ), - edit: privilege.permissions?.some(({ action }) => - action.includes(ProjectPermissionActions.Edit) - ), - create: privilege.permissions?.some(({ action }) => - action.includes(ProjectPermissionActions.Create) - ), - delete: privilege.permissions?.some(({ action }) => - action.includes(ProjectPermissionActions.Delete) - ), - // zod will pick it - temporaryAccess: privilege + ...(privilege + ? { + environmentSlug: privilege.permissions?.[0]?.conditions?.environment, + // secret path will be inside $glob operator + secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob || "", + read: privilege.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Read) + ), + edit: privilege.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Edit) + ), + create: privilege.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Create) + ), + delete: privilege.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Delete) + ), + // zod will pick it + temporaryAccess: privilege + } + : { + environmentSlug: currentWorkspace?.environments?.[0].slug!, + read: false, + edit: false, + create: false, + delete: false, + temporaryAccess: { + isTemporary: false + } + }) } }); const temporaryAccessField = privilegeForm.watch("temporaryAccess"); const selectedEnvironmentSlug = privilegeForm.watch("environmentSlug"); + const selectedEnvironment = privilegeForm.watch("environmentSlug"); + const secretPath = privilegeForm.watch("secretPath"); + + const readAccess = privilegeForm.watch("read"); + const createAccess = privilegeForm.watch("create"); + const editAccess = privilegeForm.watch("edit"); + const deleteAccess = privilegeForm.watch("delete"); + + const accessSelected = readAccess || createAccess || editAccess || deleteAccess; + + const selectablePaths = useMemo(() => { + if (!policies) return []; + const environmentPolicies = policies.filter( + (policy) => policy.environment.slug === selectedEnvironment + ); + + privilegeForm.setValue("secretPath", "", { + shouldValidate: true + }); + + return [...environmentPolicies.map((policy) => policy.secretPath)]; + }, [policies, selectedEnvironment]); + const isTemporary = temporaryAccessField?.isTemporary; const isExpired = temporaryAccessField.isTemporary && new Date() > new Date(temporaryAccessField.temporaryAccessEndTime || ""); const handleUpdatePrivilege = async (data: TSecretPermissionForm) => { + if (!privilege) { + createNotification({ + type: "error", + text: "No privilege to update found.", + title: "Error" + }); + + return; + } + if (updateUserPrivilege.isLoading) return; try { const actions = [ @@ -152,6 +212,15 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri }; const handleDeletePrivilege = async () => { + if (!privilege) { + createNotification({ + type: "error", + text: "No privilege to delete found.", + title: "Error" + }); + return; + } + if (deleteUserPrivilege.isLoading) return; try { await deleteUserPrivilege.mutateAsync({ @@ -170,35 +239,122 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri } }; + const getAccessLabel = (exactTime = false) => { if (isExpired) return "Access expired"; if (!temporaryAccessField?.isTemporary) return "Permanent"; + if (exactTime) return `Until ${format( new Date(temporaryAccessField.temporaryAccessEndTime || ""), "yyyy-MM-dd HH:mm:ss" )}`; return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); + }; + + }; + + // This is used for requesting access additional privileges, not directly creating a privilege! + const handleRequestAccess = async (data: TSecretPermissionForm) => { + if (!policies) return; + if (!currentWorkspace) { + createNotification({ + type: "error", + text: "No workspace found.", + title: "Error" + }); + return; + } + + if (!data.secretPath) { + createNotification({ + type: "error", + text: "Please select a secret path", + title: "Error" + }); + return; + } + + const actions = [ + { action: ProjectPermissionActions.Read, allowed: data.read }, + { action: ProjectPermissionActions.Create, allowed: data.create }, + { action: ProjectPermissionActions.Delete, allowed: data.delete }, + { action: ProjectPermissionActions.Edit, allowed: data.edit } + ]; + const conditions: Record = { environment: data.environmentSlug }; + if (data.secretPath) { + conditions.secretPath = { $glob: data.secretPath }; + } + await requestAccess.mutateAsync({ + ...data, + ...(data.temporaryAccess.isTemporary && { + temporaryAccessStartTime: data.temporaryAccess.temporaryAccessStartTime, + temporaryAccessEndTime: data.temporaryAccess.temporaryAccessEndTime, + temporaryRange: data.temporaryAccess.temporaryRange, + temporaryMode: "relative" + }), + envSlug: data.environmentSlug, + secretPath: data.secretPath, + projectSlug: currentWorkspace.slug, + projectMembershipId: projectMembership.id, + isTemporary: data.temporaryAccess.isTemporary, + permissions: actions + .filter(({ allowed }) => allowed) + .map(({ action }) => ({ + action, + subject: [ProjectPermissionSub.Secrets], + conditions + })) + }); + + createNotification({ + type: "success", + text: "Successfully requested access" + }); + privilegeForm.reset(); + if (onClose) onClose(); + }; + + const handleSubmit = async (data: TSecretPermissionForm) => { + if (privilege) { + handleUpdatePrivilege(data); + } else { + handleRequestAccess(data); + } + }; + + const getAccessLabel = (exactTime = false) => { + if (isExpired) return "Access expired"; + if (!temporaryAccessField?.isTemporary) return "Permanent"; + + if (exactTime) + return `Until ${format( + new Date(temporaryAccessField.temporaryAccessEndTime || ""), + "yyyy-MM-dd HH:mm:ss" + )}`; + return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); + }; + }; return ( -
-
-
+
+ +
( - + @@ -208,8 +364,28 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri ( - + render={({ field }) => { + console.log(policies); + if (policies) { + return ( + + + + ); + } + return ( + - )} + ); + }} />
-
+
@@ -301,7 +478,7 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri isExpired && "text-red-600" )} > - {getAccessLabel()} + {getAccessLabel(false)}
@@ -382,7 +559,8 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
- {privilegeForm.formState.isDirty ? ( + {/* eslint-disable-next-line no-nested-ternary */} + {privilegeForm.formState.isDirty && privilege ? ( <> - ) : ( + ) : // eslint-disable-next-line no-nested-ternary + privilege ? ( + ) : ( +
)}
+ {!!policies && ( + + )} Date: Wed, 3 Apr 2024 16:49:48 -0700 Subject: [PATCH 024/158] Fix: Move to project slug --- .../AccessApprovalPolicyList.tsx | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx index 834b54f00..7c7b354a8 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx @@ -21,7 +21,8 @@ import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission, - useSubscription + useSubscription, + useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useDeleteAccessApprovalPolicy, useGetWorkspaceUsers } from "@app/hooks/api"; @@ -43,12 +44,15 @@ export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => { ] as const); const { permission } = useProjectPermission(); const { subscription } = useSubscription(); + const { currentWorkspace } = useWorkspace(); const { data: members } = useGetWorkspaceUsers(workspaceId); const { data: policies, isLoading: isPoliciesLoading } = useGetAccessApprovalPolicies({ - workspaceId, + projectSlug: currentWorkspace?.slug as string, options: { - enabled: permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) + enabled: + permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) && + !!currentWorkspace?.slug } }); @@ -56,9 +60,11 @@ export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => { const handleDeletePolicy = async () => { const { id } = popUp.deletePolicy.data as TAccessApprovalPolicy; + if (!currentWorkspace?.slug) return; + try { await deleteSecretApprovalPolicy({ - workspaceId, + projectSlug: currentWorkspace?.slug, id }); createNotification({ @@ -114,6 +120,7 @@ export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => { Name Environment Secret Path + Eligible Approvers Approval Required @@ -129,21 +136,22 @@ export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => { )} - {policies?.map((policy) => ( - handlePopUpOpen("secretPolicyForm", policy)} - onDelete={() => handlePopUpOpen("deletePolicy", policy)} - /> - ))} + {!!currentWorkspace && + policies?.map((policy) => ( + handlePopUpOpen("secretPolicyForm", policy)} + onDelete={() => handlePopUpOpen("deletePolicy", policy)} + /> + ))} handlePopUpToggle("secretPolicyForm", isOpen)} members={members} From a4b6d2650a9d052b2fde1ddd39faa3deb2c60355 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:49:57 -0700 Subject: [PATCH 025/158] Fix: Move to project slug --- .../components/AccessApprovalPolicyRow.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx index 54eaae67c..8554e928e 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx @@ -22,7 +22,7 @@ import { TWorkspaceUser } from "@app/hooks/api/users/types"; type Props = { policy: TAccessApprovalPolicy; members?: TWorkspaceUser[]; - workspaceId: string; + projectSlug: string; onEdit: () => void; onDelete: () => void; }; @@ -30,7 +30,7 @@ type Props = { export const AccessApprovalPolicyRow = ({ policy, members = [], - workspaceId, + projectSlug, onEdit, onDelete }: Props) => { @@ -42,13 +42,14 @@ export const AccessApprovalPolicyRow = ({ {policy.name} {policy.environment.slug} + {policy.secretPath || "*"} { if (!isOpen) { updateAccessApprovalPolicy( { - workspaceId, + projectSlug, id: policy.id, approvers: selectedApprovers }, From ddd6adf804af8849bb81ade952b9bc107bf1dd92 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:50:16 -0700 Subject: [PATCH 026/158] Fix: Move to project slug --- .../components/AccessPolicyForm.tsx | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index 56a4bdef5..6b53e2f1e 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -32,7 +32,7 @@ type Props = { isOpen?: boolean; onToggle: (isOpen: boolean) => void; members?: TWorkspaceUser[]; - workspaceId: string; + projectSlug: string; editValues?: TAccessApprovalPolicy; }; @@ -40,7 +40,7 @@ const formSchema = z .object({ environment: z.string(), name: z.string().optional(), - secretPath: z.string().optional().nullable(), + secretPath: z.string().optional(), approvals: z.number().min(1), approvers: z.string().array().min(1) }) @@ -55,7 +55,7 @@ export const AccessPolicyForm = ({ isOpen, onToggle, members = [], - workspaceId, + projectSlug, editValues }: Props) => { const { @@ -80,10 +80,12 @@ export const AccessPolicyForm = ({ const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy(); const handleCreatePolicy = async (data: TFormSchema) => { + if (!projectSlug) return; + try { await createAccessApprovalPolicy({ ...data, - workspaceId + projectSlug }); createNotification({ type: "success", @@ -100,12 +102,14 @@ export const AccessPolicyForm = ({ }; const handleUpdatePolicy = async (data: TFormSchema) => { + if (!projectSlug) return; if (!editValues?.id) return; + try { await updateAccessApprovalPolicy({ id: editValues?.id, ...data, - workspaceId + projectSlug }); createNotification({ type: "success", @@ -154,6 +158,7 @@ export const AccessPolicyForm = ({ errorText={error?.message} > +
+ )} + /> + Date: Wed, 3 Apr 2024 16:50:32 -0700 Subject: [PATCH 027/158] Feat: Request access --- .../AccessApprovalRequest.tsx | 577 ++++++++++++++++++ 1 file changed, 577 insertions(+) create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx new file mode 100644 index 000000000..ffb8ac049 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -0,0 +1,577 @@ +/* eslint-disable no-nested-ternary */ +/* eslint-disable react/jsx-no-useless-fragment */ +import { useCallback, useMemo, useState } from "react"; +import { + faCheck, + faCheckCircle, + faChevronDown, + faLockOpen +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { formatDistance } from "date-fns"; +import { AnimatePresence, motion } from "framer-motion"; +import ms from "ms"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + EmptyState, + Modal, + ModalContent, + UpgradePlanModal +} from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useSubscription, + useWorkspace +} from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { useGetWorkspaceUsers, useReviewAccessRequest } from "@app/hooks/api"; +import { + useGetAccessApprovalPolicies, + useGetAccessApprovalRequests, + useGetAccessRequestsCount +} from "@app/hooks/api/accessApproval/queries"; +import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types"; +import { ApprovalStatus, TAccessApprovalPolicy, TWorkspaceUser } from "@app/hooks/api/types"; +import { SpecificPrivilegeSecretForm } from "@app/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection"; + +const DisplayBadge = ({ text, className }: { text: string; className?: string }) => { + return ( +
+ {text} +
+ ); +}; + +const ReviewRequestModal = ({ + isOpen, + onOpenChange, + request, + projectSlug, + selectedRequester, + selectedEnvSlug +}: { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + request: TAccessApprovalRequest & { user: TWorkspaceUser["user"] | null }; + projectSlug: string; + selectedRequester: string | undefined; + selectedEnvSlug: string | undefined; +}) => { + const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null); + + const accessDetails = { + env: request.environmentName, + // secret path will be inside $glob operator + secretPath: request.policy.secretPath, + read: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Read)), + edit: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Edit)), + create: request.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Create) + ), + delete: request.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Delete) + ), + + temporaryAccess: { + isTemporary: request.isTemporary, + temporaryRange: request.temporaryRange + } + }; + + const requestedAccess = useMemo(() => { + const access: string[] = []; + if (accessDetails.read) access.push("Read"); + if (accessDetails.edit) access.push("Edit"); + if (accessDetails.create) access.push("Create"); + if (accessDetails.delete) access.push("Delete"); + + return access.join(", "); + }, [accessDetails]); + + const getAccessLabel = () => { + if (!accessDetails.temporaryAccess.isTemporary || !accessDetails.temporaryAccess.temporaryRange) + return "Permanent"; + + // convert the range to human readable format + ms(ms(accessDetails.temporaryAccess.temporaryRange), { long: true }); + + return ( + + ); + }; + + const reviewAccessRequest = useReviewAccessRequest(); + + const handleReview = useCallback(async (status: "approved" | "rejected") => { + setIsLoading(status); + try { + await reviewAccessRequest.mutateAsync({ + requestId: request.id, + status, + projectSlug, + envSlug: selectedEnvSlug, + requestedBy: selectedRequester + }); + } catch (error) { + console.error(error); + setIsLoading(null); + return; + } + + createNotification({ + title: `Request ${status}`, + text: `The request has been ${status}`, + type: status === "approved" ? "success" : "info" + }); + + setIsLoading(null); + onOpenChange(false); + }, []); + + return ( + + +
+ + + {request.user?.firstName} {request.user?.lastName} ({request.user?.email}) + {" "} + is requesting access to the following resource + + +
+
+ Requested path: + +
+ +
+ Permissions: + +
+ +
+ Access Type: + {getAccessLabel()} +
+
+ +
+ + +
+
+
+
+ ); +}; + +const SelectAccessModal = ({ + isOpen, + onOpenChange, + policies +}: { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + policies: TAccessApprovalPolicy[]; +}) => { + return ( + + + onOpenChange(false)} policies={policies} /> + + + ); +}; + +const generateRequestText = (request: TAccessApprovalRequest) => { + const { isTemporary } = request; + + return ( + + Requested {isTemporary ? "temporary" : "permanent"} access to{" "} + + {request.policy.secretPath} + + in + + {request.environmentName} + + + ); +}; + +export const AccessApprovalRequest = ({ + projectSlug, + projectId +}: { + projectSlug: string; + projectId: string; +}) => { + const [selectedRequest, setSelectedRequest] = useState< + (TAccessApprovalRequest & { user: TWorkspaceUser["user"] | null }) | null + >(null); + + const { handlePopUpOpen, popUp, handlePopUpClose } = usePopUp([ + "requestAccess", + "reviewRequest", + "upgradePlan" + ] as const); + const { permission, membership } = useProjectPermission(); + const { subscription } = useSubscription(); + const { currentWorkspace } = useWorkspace(); + + const { data: members } = useGetWorkspaceUsers(projectId); + const membersGroupById = members?.reduce>( + (prev, curr) => ({ ...prev, [curr.id]: curr }), + {} + ); + + const [statusFilter, setStatusFilter] = useState<"open" | "close">("open"); + const [requestedByFilter, setRequestedByFilter] = useState(undefined); + const [envFilter, setEnvFilter] = useState(undefined); + + console.log("requestedByFilter", requestedByFilter); + console.log("envFilter", envFilter); + + const { data: requestCount } = useGetAccessRequestsCount({ + projectSlug + }); + + const { data: policies, isLoading: policiesLoading } = useGetAccessApprovalPolicies({ + projectSlug, + options: { + enabled: + permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) && + !!projectSlug + } + }); + + const { data: requests } = useGetAccessApprovalRequests({ + projectSlug, + authorProjectMembershipId: requestedByFilter, + envSlug: envFilter + }); + + const filteredRequests = useMemo(() => { + if (statusFilter === "open") return requests?.filter((request) => !request.isApproved); + if (statusFilter === "close") return requests?.filter((request) => request.isApproved); + + return requests; + }, [requests, statusFilter, requestedByFilter, envFilter]); + + const generateRequestDetails = (request: TAccessApprovalRequest) => { + const isReviewedByUser = + request.reviewers.findIndex(({ member }) => member === membership.id) !== -1; + const isRejectedByAnyone = request.reviewers.some( + ({ status }) => status === ApprovalStatus.REJECTED + ); + const isApprover = request.policy.approvers.indexOf(membership.id || "") !== -1; + const isAccepted = request.isApproved; + + const userReviewStatus = request.reviewers.find( + ({ member }) => member === membership.id + )?.status; + + let displayData: { label: string; colorClass: string } = { label: "", colorClass: "" }; + + const isExpired = + request.privilege && + request.isApproved && + new Date() > new Date(request.privilege.temporaryAccessEndTime || ("" as string)); + + if (isExpired) displayData = { label: "Access Expired", colorClass: "bg-red/20 text-red" }; + else if (isAccepted) + displayData = { label: "Access Granted", colorClass: "bg-green/20 text-green" }; + else if (isRejectedByAnyone) + displayData = { label: "Rejected", colorClass: "bg-red/20 text-red" }; + else if (userReviewStatus === ApprovalStatus.APPROVED) + displayData = { + label: `Pending ${request.policy.approvals - request.reviewers.length} reviews`, + colorClass: "bg-yellow/20 text-yellow" + }; + else if (!isReviewedByUser) + displayData = { + label: "Review Required", + colorClass: "bg-yellow/20 text-yellow" + }; + + return { + displayData, + isReviewedByUser, + isRejectedByAnyone, + isApprover, + userReviewStatus, + isAccepted + }; + }; + + return ( +
+
+
+ Access Approval Requests +
+ Request access to secrets in sensitive environments and folders. +
+
+
+ + {(isAllowed) => ( + + )} + +
+
+ + + +
+
setStatusFilter("open")} + onKeyDown={(evt) => { + if (evt.key === "Enter") setStatusFilter("open"); + }} + className={ + statusFilter === "close" ? "text-gray-500 duration-100 hover:text-gray-400" : "" + } + > + + {!!requestCount && requestCount?.pendingCount} Pending +
+
setStatusFilter("close")} + onKeyDown={(evt) => { + if (evt.key === "Enter") setStatusFilter("close"); + }} + > + + {!!requestCount && requestCount.finalizedCount} Completed +
+
+ + + + + + Select an environment + {currentWorkspace?.environments.map(({ slug, name }) => ( + setEnvFilter((state) => (state === slug ? undefined : slug))} + key={`request-filter-${slug}`} + icon={envFilter === slug && } + iconPos="right" + > + {name} + + ))} + + + + + + + + Select an author + {members?.map(({ user, id }) => ( + + setRequestedByFilter((state) => (state === id ? undefined : id)) + } + key={`request-filter-member-${id}`} + icon={requestedByFilter === id && } + iconPos="right" + > + {user.email} + + ))} + + +
+
+
+ {filteredRequests?.length === 0 && ( +
+ +
+ )} + {!!filteredRequests?.length && + requests?.map((request) => { + const details = generateRequestDetails(request); + + return ( +
{ + if (details.isReviewedByUser || details.isRejectedByAnyone) return; + + setSelectedRequest({ + ...request, + user: membersGroupById?.[request.requestedBy].user! + }); + handlePopUpOpen("reviewRequest"); + }} + onKeyDown={(evt) => { + if (evt.key === "Enter") { + setSelectedRequest({ + ...request, + user: membersGroupById?.[request.requestedBy].user! + }); + handlePopUpOpen("reviewRequest"); + } + }} + > +
+
+ + {generateRequestText(request)} +
+ + Requested {formatDistance(new Date(request.createdAt), new Date())} ago by{" "} + {membersGroupById?.[request.requestedBy]?.user?.firstName}{" "} + {membersGroupById?.[request.requestedBy]?.user?.lastName} ( + {membersGroupById?.[request.requestedBy]?.user?.email}){" "} + + {details.isApprover && + !details.isReviewedByUser && + !details.isAccepted && + "- Review required"} + + + + {details.isApprover && ( + + )} +
+
+ ); + })} +
+
+
+ + {!!policies && ( + handlePopUpClose("requestAccess")} + /> + )} + + {!!selectedRequest && ( + { + handlePopUpClose("reviewRequest"); + setSelectedRequest(null); + }} + /> + )} + + handlePopUpClose("upgradePlan")} + /> +
+ ); +}; From 0e95c1bcee020e072f488ec4b6521f3bba68fc08 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:50:47 -0700 Subject: [PATCH 028/158] Feat: Request access --- .../components/AccessApprovalPolicyRow.tsx | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx new file mode 100644 index 000000000..8554e928e --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx @@ -0,0 +1,146 @@ +import { useState } from "react"; +import { faCheckCircle, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + IconButton, + Input, + Td, + Tr +} from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { useUpdateAccessApprovalPolicy } from "@app/hooks/api"; +import { TAccessApprovalPolicy } from "@app/hooks/api/types"; +import { TWorkspaceUser } from "@app/hooks/api/users/types"; + +type Props = { + policy: TAccessApprovalPolicy; + members?: TWorkspaceUser[]; + projectSlug: string; + onEdit: () => void; + onDelete: () => void; +}; + +export const AccessApprovalPolicyRow = ({ + policy, + members = [], + projectSlug, + onEdit, + onDelete +}: Props) => { + const [selectedApprovers, setSelectedApprovers] = useState([]); + const { mutate: updateAccessApprovalPolicy, isLoading } = useUpdateAccessApprovalPolicy(); + const { permission } = useProjectPermission(); + + return ( + + {policy.name} + {policy.environment.slug} + {policy.secretPath || "*"} + + { + if (!isOpen) { + updateAccessApprovalPolicy( + { + projectSlug, + id: policy.id, + approvers: selectedApprovers + }, + { + onSettled: () => { + setSelectedApprovers([]); + } + } + ); + } else { + setSelectedApprovers(policy.approvers); + } + }} + > + + + + + + Select members that are allowed to 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} + +
+ + {(isAllowed) => ( + + + + )} + + + {(isAllowed) => ( + + + + )} + +
+ + + ); +}; From ac24c0f760f847fb5ed1b35623be364da4700e1c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:50:56 -0700 Subject: [PATCH 029/158] Feat: Request access --- .../components/AccessPolicyForm.tsx | 261 ++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx new file mode 100644 index 000000000..9cf756c14 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx @@ -0,0 +1,261 @@ +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 { createNotification } from "@app/components/notifications"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { + useCreateAccessApprovalPolicy, + useUpdateAccessApprovalPolicy +} from "@app/hooks/api/accessApproval"; +import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types"; +import { TWorkspaceUser } from "@app/hooks/api/users/types"; + +type Props = { + isOpen?: boolean; + onToggle: (isOpen: boolean) => void; + members?: TWorkspaceUser[]; + projectSlug: string; + editValues?: TAccessApprovalPolicy; +}; + +const formSchema = z + .object({ + environment: z.string(), + name: z.string().optional(), + secretPath: z.string().optional(), + approvals: z.number().min(1), + approvers: z.string().array().min(1) + }) + .refine((data) => data.approvals <= data.approvers.length, { + path: ["approvals"], + message: "The number of approvals should be lower than the number of approvers." + }); + +type TFormSchema = z.infer; + +export const AccessPolicyForm = ({ + isOpen, + onToggle, + members = [], + projectSlug, + editValues +}: Props) => { + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema), + values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined + }); + const { currentWorkspace } = useWorkspace(); + + const environments = currentWorkspace?.environments || []; + useEffect(() => { + if (!isOpen) reset({}); + }, [isOpen]); + + const isEditMode = Boolean(editValues); + + const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy(); + const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy(); + + const handleCreatePolicy = async (data: TFormSchema) => { + try { + await createAccessApprovalPolicy({ + ...data, + projectSlug + }); + 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 updateAccessApprovalPolicy({ + id: editValues?.id, + ...data, + projectSlug + }); + 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 are allowed to 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} + + ); + })} + + + + )} + /> + ( + + field.onChange(parseInt(el.target.value, 10))} + /> + + )} + /> +
+ + +
+ +
+
+ ); +}; From 7f1c8d9ff649cad65da22599e562d2ac705fd8a2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:51:02 -0700 Subject: [PATCH 030/158] Create index.tsx --- .../components/AccessApprovalRequest/index.tsx | 1 + 1 file changed, 1 insertion(+) create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/index.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/index.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/index.tsx new file mode 100644 index 000000000..ec0a8d744 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/index.tsx @@ -0,0 +1 @@ +export { AccessApprovalRequest } from "./AccessApprovalRequest"; From 17587ff1b881a29a5860a1d71b1ae0f461f46f48 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:51:13 -0700 Subject: [PATCH 031/158] Fix: Minor fixes --- .../SecretApprovalPolicyList/SecretApprovalPolicyList.tsx | 2 -- .../components/SecretApprovalRequest/SecretApprovalRequest.tsx | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx index 345ff7d52..d3a36988c 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx @@ -46,7 +46,6 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => { ] as const); const { permission } = useProjectPermission(); const { subscription } = useSubscription(); - const { data: members } = useGetWorkspaceUsers(workspaceId); const { data: policies, isLoading: isPoliciesLoading } = useGetSecretApprovalPolicies({ @@ -120,7 +119,6 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => { Secret Path Eligible Approvers Approval Required - diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx index 90133803b..96976d140 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx @@ -183,7 +183,7 @@ export const SecretApprovalRequest = () => {
-
+
{isRequestListEmpty && (
From ac92a916b47c6f895e0754eb74db3589d02eefae Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:51:21 -0700 Subject: [PATCH 032/158] Update SecretApprovalPage.tsx --- .../views/SecretApprovalPage/SecretApprovalPage.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index ebbfd7b5e..35ab35f94 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -7,6 +7,7 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { AccessApprovalPolicyList } from "./components/AccessApprovalPolicyList"; +import { AccessApprovalRequest } from "./components/AccessApprovalRequest"; import { SecretApprovalPolicyList } from "./components/SecretApprovalPolicyList"; import { SecretApprovalRequest } from "./components/SecretApprovalRequest"; @@ -19,7 +20,8 @@ enum TabSection { export const SecretApprovalPage = () => { const { currentWorkspace } = useWorkspace(); - const workspaceId = currentWorkspace?.id || ""; + const projectId = currentWorkspace?.id || ""; + const projectSlug = currentWorkspace?.slug || ""; return (
@@ -55,11 +57,15 @@ export const SecretApprovalPage = () => { - + + + + + - +
From 352d363bd4e3b159e891cbab10f97c6c2d4d90ed Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:53:02 -0700 Subject: [PATCH 033/158] Update generate-schema-types.ts --- backend/scripts/generate-schema-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 28b736152..8c913991f 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -5,7 +5,7 @@ import knex from "knex"; import { writeFileSync } from "fs"; dotenv.config({ - path: path.join(__dirname, "../../.env") + path: path.join(__dirname, "../../.env.migration") }); const db = knex({ From aedc1f2441a5681e9d356e1daadb7f435f80ae68 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:13:23 -0700 Subject: [PATCH 034/158] Update SpecificPrivilegeSection.tsx --- .../SpecificPrivilegeSection.tsx | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx index 89058b08d..b0c312f09 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -239,21 +239,6 @@ export const SpecificPrivilegeSecretForm = ({ } }; - - const getAccessLabel = (exactTime = false) => { - if (isExpired) return "Access expired"; - if (!temporaryAccessField?.isTemporary) return "Permanent"; - - if (exactTime) - return `Until ${format( - new Date(temporaryAccessField.temporaryAccessEndTime || ""), - "yyyy-MM-dd HH:mm:ss" - )}`; - return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); - }; - - }; - // This is used for requesting access additional privileges, not directly creating a privilege! const handleRequestAccess = async (data: TSecretPermissionForm) => { if (!policies) return; @@ -327,14 +312,13 @@ export const SpecificPrivilegeSecretForm = ({ if (isExpired) return "Access expired"; if (!temporaryAccessField?.isTemporary) return "Permanent"; - if (exactTime) + if (exactTime) { return `Until ${format( new Date(temporaryAccessField.temporaryAccessEndTime || ""), "yyyy-MM-dd HH:mm:ss" )}`; + } return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); - }; - }; return ( From 076c70f6ff697efc4a45974570f42ae198f80a72 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:19:26 -0700 Subject: [PATCH 035/158] Removed logs --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index ffb8ac049..f29b8a52f 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -276,9 +276,6 @@ export const AccessApprovalRequest = ({ const [requestedByFilter, setRequestedByFilter] = useState(undefined); const [envFilter, setEnvFilter] = useState(undefined); - console.log("requestedByFilter", requestedByFilter); - console.log("envFilter", envFilter); - const { data: requestCount } = useGetAccessRequestsCount({ projectSlug }); From 6f4b62cfbb00243b6db811859d847b0b9e89e5d2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:20:33 -0700 Subject: [PATCH 036/158] Removed logs --- .../MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx | 1 - .../components/SecretApprovalRequestChanges.tsx | 2 -- 2 files changed, 3 deletions(-) diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx index b0c312f09..2993704bb 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -349,7 +349,6 @@ export const SpecificPrivilegeSecretForm = ({ control={privilegeForm.control} name="secretPath" render={({ field }) => { - console.log(policies); if (policies) { return ( diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx index e1437b563..80dbe9f73 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx @@ -83,7 +83,6 @@ export const SecretApprovalRequestChanges = ({ workspaceId, members = {} }: Props) => { - const { user } = useUser(); const { data: decryptFileKey } = useGetUserWsKey(workspaceId); const { @@ -94,7 +93,6 @@ export const SecretApprovalRequestChanges = ({ id: approvalRequestId, decryptKey: decryptFileKey! }); - console.log(secretApprovalRequestDetails); const { mutateAsync: updateSecretApprovalRequestStatus, From 890c8b89bead0faaa43a5ea027dac4be6ca127e6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:41:17 -0700 Subject: [PATCH 037/158] Removed unused parameter --- .../ee/routes/v1/access-approval-request-router.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 696c8b76f..2a6142c88 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -1,9 +1,7 @@ -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas"; import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; -import { alphaNumericNanoId } from "@app/lib/nanoid"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -13,16 +11,6 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv method: "POST", schema: { body: z.object({ - slug: z - .string() - .min(1) - .max(60) - .trim() - .default(`requested-privilege-${slugify(alphaNumericNanoId(12))}`) - .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }), permissions: z.any().array(), isTemporary: z.boolean(), temporaryRange: z.string().optional() From e6c086ab09d93db17e34c516a5d1cc2ce1b9bdd3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:42:14 -0700 Subject: [PATCH 038/158] Fix: Don't allow users to request access to the same resource with same permissions multiple times --- .../access-approval-request-service.ts | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index d02842939..3640d801e 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -1,8 +1,10 @@ import { ForbiddenError } from "@casl/ability"; +import slugify from "@sindresorhus/slugify"; import ms from "ms"; import { ProjectMembershipRole } from "@app/db/schemas"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; @@ -64,9 +66,15 @@ export const accessApprovalRequestServiceFactory = ({ if (!project) throw new UnauthorizedError({ message: "Project not found" }); // Anyone can create an access approval request. - const p = await permissionService.getProjectPermission(actor, actorId, project.id, actorAuthMethod, actorOrgId); + const { membership } = await permissionService.getProjectPermission( + actor, + actorId, + project.id, + actorAuthMethod, + actorOrgId + ); - if (!p.membership) throw new UnauthorizedError({ message: "You are not a member of this project" }); + if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" }); await projectDAL.checkProjectUpgradeStatus(project.id); @@ -81,9 +89,27 @@ export const accessApprovalRequestServiceFactory = ({ }); if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." }); + const duplicateRequest = await accessApprovalRequestDAL.findOne({ + policyId: policy.id, + requestedBy: membership.id, + permissions: JSON.stringify(permissions), + temporaryRange: temporaryRange || null, + isTemporary + }); + + if (duplicateRequest.privilegeId) { + const privilege = await additionalPrivilegeDAL.findById(duplicateRequest.privilegeId); + + const isExpired = new Date() > new Date(privilege.temporaryAccessEndTime || ("" as string)); + + if (!isExpired) { + throw new BadRequestError({ message: "You have already requested access with the same permissions" }); + } + } + const approvalRequest = await accessApprovalRequestDAL.create({ policyId: policy.id, - requestedBy: p.membership.id, + requestedBy: membership.id, temporaryRange: temporaryRange || null, permissions: JSON.stringify(permissions), isTemporary @@ -209,7 +235,7 @@ export const accessApprovalRequestServiceFactory = ({ // Permanent access const privilege = await additionalPrivilegeDAL.create({ projectMembershipId: accessApprovalRequest.requestedBy, - slug: "", + slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, permissions: JSON.stringify(accessApprovalRequest.permissions) }); privilegeId = privilege.id; @@ -220,7 +246,7 @@ export const accessApprovalRequestServiceFactory = ({ const privilege = await additionalPrivilegeDAL.create({ projectMembershipId: accessApprovalRequest.requestedBy, - slug: "", + slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, permissions: JSON.stringify(accessApprovalRequest.permissions), isTemporary: true, temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, From d371c568f1beac2e53bd8d8415bb643280e43efb Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:42:27 -0700 Subject: [PATCH 039/158] Add count --- .../src/hooks/api/accessApproval/mutation.tsx | 5 ++++- frontend/src/layouts/AppLayout/AppLayout.tsx | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index 7a150092c..c81295e60 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -88,7 +88,10 @@ export const useCreateAccessRequest = () => { return data; }, onSuccess: (_, { projectSlug }) => { - queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequests(projectSlug)); + queryClient.invalidateQueries([ + accessApprovalKeys.getAccessApprovalRequests(projectSlug), + accessApprovalKeys.getAccessApprovalRequestCount(projectSlug) + ]); } }); }; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index a6c25c7c2..2fcdc9339 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -5,7 +5,7 @@ /* eslint-disable no-var */ /* eslint-disable func-names */ -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import Image from "next/image"; @@ -64,6 +64,7 @@ import { fetchOrgUsers, useAddUserToWsNonE2EE, useCreateWorkspace, + useGetAccessRequestsCount, useGetOrgTrialUrl, useGetSecretApprovalRequestCount, useGetUserAction, @@ -124,9 +125,15 @@ export const AppLayout = ({ children }: LayoutProps) => { const { user } = useUser(); const { subscription } = useSubscription(); const workspaceId = currentWorkspace?.id || ""; + const projectSlug = currentWorkspace?.slug || ""; const { data: updateClosed } = useGetUserAction("december_update_closed"); const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId }); + const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug }); + + const pendingRequestsCount = useMemo(() => { + return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0); + }, [secretApprovalReqCount, accessApprovalRequestCount]); const isAddingProjectsAllowed = subscription?.workspaceLimit ? subscription.workspacesUsed < subscription.workspaceLimit @@ -555,9 +562,12 @@ export const AppLayout = ({ children }: LayoutProps) => { icon="system-outline-189-domain-verification" > Approvals - {Boolean(secretApprovalReqCount?.open) && ( + {Boolean( + secretApprovalReqCount?.open || + accessApprovalRequestCount?.pendingCount + ) && ( - {secretApprovalReqCount?.open} + {pendingRequestsCount} )} From 073a9ee6a469dd7dc83b4ec27f1071fff3fd6d40 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:43:45 -0700 Subject: [PATCH 040/158] Update licence-fns.ts --- backend/src/ee/services/license/licence-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 492d09ec3..189a3c4e0 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ status: null, trial_end: null, has_used_trial: true, - secretApproval: true, + secretApproval: false, secretRotation: true }); From 15c747e8e8bacce64b3f85c9b76416db5b0c5611 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:48:27 -0700 Subject: [PATCH 041/158] Fix: Request access permissions --- .../access-approval-policy-service.ts | 9 ++++++++- .../access-approval-request-service.ts | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index 30f5f10fe..d46f93e1d 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -248,7 +248,14 @@ export const accessApprovalPolicyServiceFactory = ({ if (!project) throw new BadRequestError({ message: "Project not found" }); - await permissionService.getProjectPermission(actor, actorId, project.id, actorAuthMethod, actorOrgId); + const { membership } = await permissionService.getProjectPermission( + actor, + actorId, + project.id, + actorAuthMethod, + actorOrgId + ); + if (!membership) throw new BadRequestError({ message: "User not found in project" }); const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); if (!environment) throw new BadRequestError({ message: "Environment not found" }); diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 3640d801e..780e98655 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -272,14 +272,14 @@ export const accessApprovalRequestServiceFactory = ({ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new UnauthorizedError({ message: "Project not found" }); - const { permission } = await permissionService.getProjectPermission( + const { membership } = await permissionService.getProjectPermission( actor, actorId, project.id, actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + if (!membership) throw new BadRequestError({ message: "User not found in project" }); const count = await accessApprovalRequestDAL.getCount({ projectId: project.id }); From b59413ded00731f2bb65c5214e1ee6ca10cb7398 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 3 Apr 2024 19:47:59 -0700 Subject: [PATCH 042/158] fix privilegeId issue --- .../access-approval-request/access-approval-request-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 780e98655..230431d18 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -97,7 +97,7 @@ export const accessApprovalRequestServiceFactory = ({ isTemporary }); - if (duplicateRequest.privilegeId) { + if (duplicateRequest?.privilegeId) { const privilege = await additionalPrivilegeDAL.findById(duplicateRequest.privilegeId); const isExpired = new Date() > new Date(privilege.temporaryAccessEndTime || ("" as string)); From f8e0e01bb89328c4af02cc811bda414ce0489812 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:02:37 -0700 Subject: [PATCH 043/158] Fix: Access request query invalidation --- frontend/src/hooks/api/accessApproval/mutation.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index c81295e60..518729aa5 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -89,7 +89,6 @@ export const useCreateAccessRequest = () => { }, onSuccess: (_, { projectSlug }) => { queryClient.invalidateQueries([ - accessApprovalKeys.getAccessApprovalRequests(projectSlug), accessApprovalKeys.getAccessApprovalRequestCount(projectSlug) ]); } From 8c256bd9c87f2d67c4c8c1be87ccbc36586b6e70 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:02:54 -0700 Subject: [PATCH 044/158] Fix: Status filtering & query invalidation --- .../AccessApprovalRequest.tsx | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index f29b8a52f..8fe69a2d0 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -37,12 +37,14 @@ import { import { usePopUp } from "@app/hooks"; import { useGetWorkspaceUsers, useReviewAccessRequest } from "@app/hooks/api"; import { + accessApprovalKeys, useGetAccessApprovalPolicies, useGetAccessApprovalRequests, useGetAccessRequestsCount } from "@app/hooks/api/accessApproval/queries"; import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types"; import { ApprovalStatus, TAccessApprovalPolicy, TWorkspaceUser } from "@app/hooks/api/types"; +import { queryClient } from "@app/reactQuery"; import { SpecificPrivilegeSecretForm } from "@app/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection"; const DisplayBadge = ({ text, className }: { text: string; className?: string }) => { @@ -296,8 +298,18 @@ export const AccessApprovalRequest = ({ }); const filteredRequests = useMemo(() => { - if (statusFilter === "open") return requests?.filter((request) => !request.isApproved); - if (statusFilter === "close") return requests?.filter((request) => request.isApproved); + if (statusFilter === "open") + return requests?.filter( + (request) => + !request.isApproved && + !request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED) + ); + if (statusFilter === "close") + return requests?.filter( + (request) => + request.isApproved || + request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED) + ); return requests; }, [requests, statusFilter, requestedByFilter, envFilter]); @@ -481,7 +493,7 @@ export const AccessApprovalRequest = ({
)} {!!filteredRequests?.length && - requests?.map((request) => { + filteredRequests?.map((request) => { const details = generateRequestDetails(request); return ( @@ -546,7 +558,16 @@ export const AccessApprovalRequest = ({ handlePopUpClose("requestAccess")} + onOpenChange={() => { + queryClient.invalidateQueries( + accessApprovalKeys.getAccessApprovalRequests( + projectSlug, + envFilter, + requestedByFilter + ) + ); + handlePopUpClose("requestAccess"); + }} /> )} From 299653528c4ddb7fe588072d7f3f5e6e0d1177db Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 3 Apr 2024 20:24:33 -0700 Subject: [PATCH 045/158] style changes --- .../SecretApprovalPage/SecretApprovalPage.tsx | 8 +- .../AccessApprovalPolicyList.tsx | 6 +- .../AccessApprovalRequest.tsx | 73 +++++++++---------- 3 files changed, 42 insertions(+), 45 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 35ab35f94..a12881793 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -47,11 +47,11 @@ export const SecretApprovalPage = () => {
- Secret Approvals - Secret Policies + Change Requests + Change Request Policies - Access Approvals - Access Policies + Access Requests + Access Request Policies diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx index 7c7b354a8..ba12c5189 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx @@ -83,11 +83,11 @@ export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => { return (
-
+
- Access Approval Policies + Access Request Policies
- Implement policies to prevent unauthorized secret changes. + Implement secret request policies for specific secrets and environments.
diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 8fe69a2d0..eb8b886f2 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -5,7 +5,8 @@ import { faCheck, faCheckCircle, faChevronDown, - faLockOpen + faLock, + faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { formatDistance } from "date-fns"; @@ -25,6 +26,7 @@ import { EmptyState, Modal, ModalContent, + Tooltip, UpgradePlanModal } from "@app/components/v2"; import { @@ -51,7 +53,7 @@ const DisplayBadge = ({ text, className }: { text: string; className?: string }) return (
@@ -155,28 +157,28 @@ const ReviewRequestModal = ({
{request.user?.firstName} {request.user?.lastName} ({request.user?.email}) {" "} - is requesting access to the following resource + is requesting access to the following resource: -
-
+
+
Requested path:
-
+
Permissions:
-
+
Access Type: {getAccessLabel()}
@@ -188,7 +190,7 @@ const ReviewRequestModal = ({ isDisabled={!!isLoading} onClick={() => handleReview("approved")} className="mt-4" - size="xs" + size="sm" > Approve Request @@ -196,9 +198,8 @@ const ReviewRequestModal = ({ isLoading={isLoading === "rejected"} isDisabled={!!isLoading} onClick={() => handleReview("rejected")} - className="mt-4" - size="xs" - colorSchema="danger" + className="mt-4 bg-transparent border-transparent hover:bg-red/20 hover:border-red text-mineshaft-200 hover:text-mineshaft-200" + size="sm" > Reject Request @@ -237,11 +238,11 @@ const generateRequestText = (request: TAccessApprovalRequest) => { return ( Requested {isTemporary ? "temporary" : "permanent"} access to{" "} - + {request.policy.secretPath} in - + {request.environmentName} @@ -362,9 +363,9 @@ export const AccessApprovalRequest = ({ return (
-
+
- Access Approval Requests + Access Requests
Request access to secrets in sensitive environments and folders.
@@ -375,19 +376,21 @@ export const AccessApprovalRequest = ({ a={ProjectPermissionSub.SecretApproval} > {(isAllowed) => ( - + + + )}
@@ -414,7 +417,7 @@ export const AccessApprovalRequest = ({ statusFilter === "close" ? "text-gray-500 duration-100 hover:text-gray-400" : "" } > - + {!!requestCount && requestCount?.pendingCount} Pending
)} {!!filteredRequests?.length && - filteredRequests?.map((request) => { + requests?.map((request) => { const details = generateRequestDetails(request); return ( @@ -524,7 +527,7 @@ export const AccessApprovalRequest = ({ >
- + {generateRequestText(request)}
@@ -532,12 +535,6 @@ export const AccessApprovalRequest = ({ {membersGroupById?.[request.requestedBy]?.user?.firstName}{" "} {membersGroupById?.[request.requestedBy]?.user?.lastName} ( {membersGroupById?.[request.requestedBy]?.user?.email}){" "} - - {details.isApprover && - !details.isReviewedByUser && - !details.isAccepted && - "- Review required"} - {details.isApprover && ( From 3a002b921aebad865c80617e0ccf88f1afd7b62c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:29:20 -0700 Subject: [PATCH 046/158] Update AccessApprovalRequest.tsx --- .../AccessApprovalRequest/AccessApprovalRequest.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index eb8b886f2..8ac0b5b78 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -167,7 +167,7 @@ const ReviewRequestModal = ({ is requesting access to the following resource: -
+
Requested path: @@ -198,7 +198,7 @@ const ReviewRequestModal = ({ isLoading={isLoading === "rejected"} isDisabled={!!isLoading} onClick={() => handleReview("rejected")} - className="mt-4 bg-transparent border-transparent hover:bg-red/20 hover:border-red text-mineshaft-200 hover:text-mineshaft-200" + className="mt-4 border-transparent bg-transparent text-mineshaft-200 hover:border-red hover:bg-red/20 hover:text-mineshaft-200" size="sm" > Reject Request @@ -363,7 +363,7 @@ export const AccessApprovalRequest = ({ return (
-
+
Access Requests
@@ -376,7 +376,10 @@ export const AccessApprovalRequest = ({ a={ProjectPermissionSub.SecretApproval} > {(isAllowed) => ( - +
)} {!!filteredRequests?.length && - requests?.map((request) => { + filteredRequests?.map((request) => { const details = generateRequestDetails(request); return ( From 8c40918cef5d64aaf74b7c8760758c7d0fcfa078 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:33:58 -0700 Subject: [PATCH 047/158] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 8ac0b5b78..c112a5b55 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -493,7 +493,7 @@ export const AccessApprovalRequest = ({
- {filteredRequests?.length === 0 && ( + {filteredRequests?.length === 0 && ( //
From 4cb51805f02fcd50b6787c255eec2b7a533b030f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:34:11 -0700 Subject: [PATCH 048/158] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index c112a5b55..8ac0b5b78 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -493,7 +493,7 @@ export const AccessApprovalRequest = ({
- {filteredRequests?.length === 0 && ( // + {filteredRequests?.length === 0 && (
From f1428d72c2bb4eeecbac2c23b4e54b8f1009bde0 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:00:14 -0700 Subject: [PATCH 049/158] Fix: Security vulnurbility making it possible to spoof env & secret path requested. --- backend/src/ee/routes/v1/access-approval-request-router.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 2a6142c88..4b173cfa7 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -16,9 +16,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv temporaryRange: z.string().optional() }), querystring: z.object({ - projectSlug: z.string().trim(), - secretPath: z.string().trim(), - envSlug: z.string().trim() + projectSlug: z.string().trim() }), response: { 200: z.object({ @@ -33,9 +31,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, permissions: req.body.permissions, - envSlug: req.query.envSlug, actorOrgId: req.permission.orgId, - secretPath: req.query.secretPath, projectSlug: req.query.projectSlug, temporaryRange: req.body.temporaryRange, isTemporary: req.body.isTemporary From fb2ab200b9650cac0a44750d7168033ca4516f17 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:00:39 -0700 Subject: [PATCH 050/158] Feat: Request access, extract permission details --- .../access-approval-request-fns.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-fns.ts diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-fns.ts b/backend/src/ee/services/access-approval-request/access-approval-request-fns.ts new file mode 100644 index 000000000..90b42aaf7 --- /dev/null +++ b/backend/src/ee/services/access-approval-request/access-approval-request-fns.ts @@ -0,0 +1,53 @@ +import { PackRule, unpackRules } from "@casl/ability/extra"; + +import { UnauthorizedError } from "@app/lib/errors"; + +import { TVerifyPermission } from "./access-approval-request-types"; + +function filterUnique(value: string, index: number, array: string[]) { + return array.indexOf(value) === index; +} + +export const verifyRequestedPermissions = ({ permissions }: TVerifyPermission) => { + const permission = unpackRules( + permissions as PackRule<{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + conditions?: Record; + action: string; + subject: [string]; + }>[] + ); + + if (!permission || !permission.length) { + throw new UnauthorizedError({ message: "No permission provided" }); + } + + const requestedPermissions: string[] = []; + + for (const p of permission) { + if (p.action[0] === "read") requestedPermissions.push("Read Access"); + if (p.action[0] === "create") requestedPermissions.push("Create Access"); + if (p.action[0] === "delete") requestedPermissions.push("Delete Access"); + if (p.action[0] === "edit") requestedPermissions.push("Edit Access"); + } + + const firstPermission = permission[0]; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const permissionSecretPath = firstPermission.conditions?.secretPath?.$glob; + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unsafe-assignment + const permissionEnv = firstPermission.conditions?.environment; + + if (!permissionEnv || typeof permissionEnv !== "string") { + throw new UnauthorizedError({ message: "Permission environment is not a string" }); + } + if (!permissionSecretPath || typeof permissionSecretPath !== "string") { + throw new UnauthorizedError({ message: "Permission path is not a string" }); + } + + return { + envSlug: permissionEnv, + secretPath: permissionSecretPath, + accessTypes: requestedPermissions.filter(filterUnique) + }; +}; From 6ae62675be59bbd60c1eb5935f487a5e1a03921d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:01:14 -0700 Subject: [PATCH 051/158] Feat: Send emails for access requests --- .../access-approval-request-service.ts | 103 +++++++++++++----- 1 file changed, 77 insertions(+), 26 deletions(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 230431d18..0c170c40f 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -1,22 +1,25 @@ -import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; import ms from "ms"; import { ProjectMembershipRole } from "@app/db/schemas"; +import { getConfig } from "@app/lib/config/env"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; +import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; +import { TUserDALFactory } from "@app/services/user/user-dal"; +import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal"; import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeServiceFactory } from "../project-user-additional-privilege/project-user-additional-privilege-service"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types"; import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal"; +import { verifyRequestedPermissions } from "./access-approval-request-fns"; import { TAccessApprovalRequestReviewerDALFactory } from "./access-approval-request-reviewer-dal"; import { ApprovalStatus, @@ -30,12 +33,15 @@ type TSecretApprovalRequestServiceFactoryDep = { additionalPrivilegeService: TProjectUserAdditionalPrivilegeServiceFactory; additionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory; permissionService: TPermissionServiceFactory; + accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory; projectEnvDAL: Pick; projectDAL: Pick; accessApprovalRequestDAL: TAccessApprovalRequestDALFactory; accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory; accessApprovalRequestReviewerDAL: TAccessApprovalRequestReviewerDALFactory; projectMembershipDAL: TProjectMembershipDALFactory; + smtpService: TSmtpService; + userDAL: TUserDALFactory; }; export type TAccessApprovalRequestServiceFactory = ReturnType; @@ -48,20 +54,22 @@ export const accessApprovalRequestServiceFactory = ({ accessApprovalRequestReviewerDAL, projectMembershipDAL, accessApprovalPolicyDAL, - additionalPrivilegeDAL + accessApprovalPolicyApproverDAL, + additionalPrivilegeDAL, + smtpService, + userDAL }: TSecretApprovalRequestServiceFactoryDep) => { const createAccessApprovalRequest = async ({ isTemporary, temporaryRange, actorId, - envSlug, - permissions, - secretPath, + permissions: requestedPermissions, actor, actorOrgId, actorAuthMethod, projectSlug }: TCreateAccessApprovalRequestDTO) => { + const cfg = getConfig(); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new UnauthorizedError({ message: "Project not found" }); @@ -73,15 +81,17 @@ export const accessApprovalRequestServiceFactory = ({ actorAuthMethod, actorOrgId ); - if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" }); + const requestedByUser = await userDAL.findUserByProjectMembershipId(membership.id); + if (!requestedByUser) throw new UnauthorizedError({ message: "User not found" }); + await projectDAL.checkProjectUpgradeStatus(project.id); + const { envSlug, secretPath, accessTypes } = verifyRequestedPermissions({ permissions: requestedPermissions }); const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); if (!environment) throw new UnauthorizedError({ message: "Environment not found" }); - if (!secretPath) throw new UnauthorizedError({ message: "Secret path is required" }); const policy = await accessApprovalPolicyDAL.findOne({ envId: environment.id, @@ -89,33 +99,75 @@ export const accessApprovalRequestServiceFactory = ({ }); if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." }); + const approvers = await accessApprovalPolicyApproverDAL.find({ + policyId: policy.id + }); + + const approverUsers = await userDAL.findUsersByProjectMembershipIds( + approvers.map((approver) => approver.approverId) + ); + + if (approverUsers.length !== approvers.length) { + throw new BadRequestError({ message: "Some approvers were not found" }); + } + const duplicateRequest = await accessApprovalRequestDAL.findOne({ policyId: policy.id, requestedBy: membership.id, - permissions: JSON.stringify(permissions), - temporaryRange: temporaryRange || null, + permissions: JSON.stringify(requestedPermissions), isTemporary }); - if (duplicateRequest?.privilegeId) { - const privilege = await additionalPrivilegeDAL.findById(duplicateRequest.privilegeId); + if (duplicateRequest) { + if (duplicateRequest.privilegeId) { + const privilege = await additionalPrivilegeDAL.findById(duplicateRequest.privilegeId); - const isExpired = new Date() > new Date(privilege.temporaryAccessEndTime || ("" as string)); + const isExpired = new Date() > new Date(privilege.temporaryAccessEndTime || ("" as string)); - if (!isExpired) { - throw new BadRequestError({ message: "You have already requested access with the same permissions" }); + if (!isExpired || !privilege.isTemporary) { + throw new BadRequestError({ message: "You already have an active privilege with the same criteria" }); + } + } else { + throw new BadRequestError({ message: "You already have a pending access request with the same criteria" }); } } - const approvalRequest = await accessApprovalRequestDAL.create({ - policyId: policy.id, - requestedBy: membership.id, - temporaryRange: temporaryRange || null, - permissions: JSON.stringify(permissions), - isTemporary + const approval = await accessApprovalRequestDAL.transaction(async (tx) => { + const approvalRequest = await accessApprovalRequestDAL.create( + { + policyId: policy.id, + requestedBy: membership.id, + temporaryRange: temporaryRange || null, + permissions: JSON.stringify(requestedPermissions), + isTemporary + }, + tx + ); + + await smtpService.sendMail({ + recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!), + subjectLine: "Access Approval Request", + + substitutions: { + projectName: project.name, + requesterFullName: `${requestedByUser.firstName} ${requestedByUser.lastName}`, + requesterEmail: requestedByUser.email, + isTemporary, + ...(isTemporary && { + expiresIn: ms(ms(temporaryRange || ""), { long: true }) + }), + secretPath, + environment: envSlug, + permissions: accessTypes, + approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval` + }, + template: SmtpTemplates.AccessApprovalRequest + }); + + return approvalRequest; }); - return { request: approvalRequest }; + return { request: approval }; }; const listApprovalRequests = async ({ @@ -130,15 +182,14 @@ export const accessApprovalRequestServiceFactory = ({ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new UnauthorizedError({ message: "Project not found" }); - const { permission } = await permissionService.getProjectPermission( + const { membership } = await permissionService.getProjectPermission( actor, actorId, project.id, actorAuthMethod, actorOrgId ); - - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" }); const policies = await accessApprovalPolicyDAL.find({ projectId: project.id }); let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id)); @@ -181,7 +232,7 @@ export const accessApprovalRequestServiceFactory = ({ accessApprovalRequest.requestedBy !== membership.id && // The request wasn't made by the current user !policy.approvers.find((approverId) => approverId === membership.id) // The request isn't performed by an assigned approver ) { - throw new UnauthorizedError({ message: "No access" }); + throw new UnauthorizedError({ message: "You are not authorized to approve this request" }); } const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id); From c7b60bcf0ea94306b531d273b264bf76dc3b64e4 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:01:22 -0700 Subject: [PATCH 052/158] Update access-approval-request-types.ts --- .../access-approval-request-types.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts index 6bba0c14d..e11ca58d5 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts @@ -6,6 +6,10 @@ export enum ApprovalStatus { REJECTED = "rejected" } +export type TVerifyPermission = { + permissions: unknown; +}; + export type TGetAccessRequestCountDTO = { projectSlug: string; } & Omit; @@ -17,8 +21,6 @@ export type TReviewAccessRequestDTO = { export type TCreateAccessApprovalRequestDTO = { projectSlug: string; - secretPath: string; - envSlug: string; permissions: unknown; isTemporary: boolean; temporaryRange?: string; From 608c7a4dee51f1ad6b1aeb41d49783a01650e368 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:01:32 -0700 Subject: [PATCH 053/158] Update index.ts --- backend/src/server/routes/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 905d4028c..63c5609be 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -628,7 +628,10 @@ export const registerRoutes = async ( additionalPrivilegeService: projectUserAdditionalPrivilegeService, accessApprovalPolicyDAL, accessApprovalRequestDAL, - projectEnvDAL + projectEnvDAL, + userDAL, + smtpService, + accessApprovalPolicyApproverDAL }); const secretRotationQueue = secretRotationQueueFactory({ From 8e1d19c0412bade9e4847feb90c4680d1c37fb42 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:02:24 -0700 Subject: [PATCH 054/158] Feat: access request emails --- .../accessApprovalRequest.handlebars | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 backend/src/services/smtp/templates/accessApprovalRequest.handlebars diff --git a/backend/src/services/smtp/templates/accessApprovalRequest.handlebars b/backend/src/services/smtp/templates/accessApprovalRequest.handlebars new file mode 100644 index 000000000..82c66ce5f --- /dev/null +++ b/backend/src/services/smtp/templates/accessApprovalRequest.handlebars @@ -0,0 +1,50 @@ + + + + + + Access Approval Request + + + +

Infisical

+

New access approval request pending your review

+

You have a new access approval request pending review in project "{{projectName}}".

+ +

+ {{requesterFullName}} + ({{requesterEmail}}) has requested + {{#if isTemporary}} + temporary + {{else}} + permanent + {{/if}} + access to + {{secretPath}} + in the + {{environment}} + environment. + + {{#if isTemporary}} +
+ This access will expire + {{expiresIn}} + after it has been approved. + {{/if}} +

+

+ The following permissions are requested: +

    + {{#each permissions}} +
  • {{this}}
  • + {{/each}} +
+

+ +

+ View the request and approve or deny it + here. +

+ + + \ No newline at end of file From c5d11eee7f47b4018e936535a34f59dfa1164e75 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:02:39 -0700 Subject: [PATCH 055/158] Feat: Find users by project membership ID's --- backend/src/services/user/user-dal.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts index 530ca3ad1..f2da0df0e 100644 --- a/backend/src/services/user/user-dal.ts +++ b/backend/src/services/user/user-dal.ts @@ -74,6 +74,17 @@ export const userDALFactory = (db: TDbClient) => { } }; + const findUsersByProjectMembershipIds = async (projectMembershipIds: string[]) => { + try { + return await db(TableName.ProjectMembership) + .whereIn(`${TableName.ProjectMembership}.id`, projectMembershipIds) + .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .select("*"); + } catch (error) { + throw new DatabaseError({ error, name: "Find users by project membership ids" }); + } + }; + const createUserEncryption = async (data: TUserEncryptionKeysInsert, tx?: Knex) => { try { const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*"); @@ -140,6 +151,7 @@ export const userDALFactory = (db: TDbClient) => { findUserEncKeyByUserId, updateUserEncryptionByUserId, findUserByProjectMembershipId, + findUsersByProjectMembershipIds, upsertUserEncryptionKey, createUserEncryption, findOneUserAction, From cb8763bc9c97eeda013a89602860f590e8ee0340 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:02:47 -0700 Subject: [PATCH 056/158] Update smtp-service.ts --- backend/src/services/smtp/smtp-service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 0b43ffb90..81680537d 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -21,6 +21,7 @@ export enum SmtpTemplates { EmailVerification = "emailVerification.handlebars", SecretReminder = "secretReminder.handlebars", EmailMfa = "emailMfa.handlebars", + AccessApprovalRequest = "accessApprovalRequest.handlebars", HistoricalSecretList = "historicalSecretLeakIncident.handlebars", NewDeviceJoin = "newDevice.handlebars", OrgInvite = "organizationInvitation.handlebars", From 54313f9c08fab5e83f72bf03aeda7cbc8a60c5da Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:02:55 -0700 Subject: [PATCH 057/158] Renaming --- frontend/src/hooks/api/accessApproval/mutation.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index 518729aa5..1bf3841ff 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -69,18 +69,16 @@ export const useDeleteAccessApprovalPolicy = () => { export const useCreateAccessRequest = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TCreateAccessRequestDTO>({ - mutationFn: async ({ envSlug, projectSlug, secretPath, ...privilege }) => { + mutationFn: async ({ projectSlug, ...request }) => { const { data } = await apiRequest.post( "/api/v1/access-approval-requests", { - ...privilege, - permissions: privilege.permissions ? packRules(privilege.permissions) : undefined + ...request, + permissions: request.permissions ? packRules(request.permissions) : undefined }, { params: { - envSlug, - projectSlug, - secretPath + projectSlug } } ); @@ -88,9 +86,7 @@ export const useCreateAccessRequest = () => { return data; }, onSuccess: (_, { projectSlug }) => { - queryClient.invalidateQueries([ - accessApprovalKeys.getAccessApprovalRequestCount(projectSlug) - ]); + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequestCount(projectSlug)); } }); }; From 478520f090f94930bf70aecf0422c2269a89d1d8 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:03:10 -0700 Subject: [PATCH 058/158] Remove unnessecary types and projectMembershipid --- frontend/src/hooks/api/accessApproval/types.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts index 1ffd0bf2e..2176b8bc1 100644 --- a/frontend/src/hooks/api/accessApproval/types.ts +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -92,10 +92,8 @@ export type TProjectUserPrivilege = { ); export type TCreateAccessRequestDTO = { - envSlug: string; projectSlug: string; - secretPath: string; -} & Omit; +} & Omit; export type TGetAccessApprovalRequestsDTO = { projectSlug: string; From 2369ff6813a29a2e214ab59598f397ae49b40b56 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:03:19 -0700 Subject: [PATCH 059/158] Removed unnessecary types --- .../MemberRoleForm/SpecificPrivilegeSection.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx index 2993704bb..88dba72a1 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -84,7 +84,6 @@ export const SpecificPrivilegeSecretForm = ({ onClose?: () => void; }) => { const { currentWorkspace } = useWorkspace(); - const { membership: projectMembership } = useProjectPermission(); const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ "deletePrivilege", @@ -271,17 +270,10 @@ export const SpecificPrivilegeSecretForm = ({ conditions.secretPath = { $glob: data.secretPath }; } await requestAccess.mutateAsync({ - ...data, ...(data.temporaryAccess.isTemporary && { - temporaryAccessStartTime: data.temporaryAccess.temporaryAccessStartTime, - temporaryAccessEndTime: data.temporaryAccess.temporaryAccessEndTime, - temporaryRange: data.temporaryAccess.temporaryRange, - temporaryMode: "relative" + temporaryRange: data.temporaryAccess.temporaryRange }), - envSlug: data.environmentSlug, - secretPath: data.secretPath, projectSlug: currentWorkspace.slug, - projectMembershipId: projectMembership.id, isTemporary: data.temporaryAccess.isTemporary, permissions: actions .filter(({ allowed }) => allowed) @@ -312,7 +304,7 @@ export const SpecificPrivilegeSecretForm = ({ if (isExpired) return "Access expired"; if (!temporaryAccessField?.isTemporary) return "Permanent"; - if (exactTime) { + if (exactTime && !policies) { return `Until ${format( new Date(temporaryAccessField.temporaryAccessEndTime || ""), "yyyy-MM-dd HH:mm:ss" From 0990ce1f9257ca8892118b79e78997b679db0980 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:03:29 -0700 Subject: [PATCH 060/158] Capitalization --- .../AccessApprovalPolicyList/components/AccessPolicyForm.tsx | 2 +- .../AccessApprovalRequest/components/AccessPolicyForm.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index 6b53e2f1e..dd51ea53d 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -135,7 +135,7 @@ export const AccessPolicyForm = ({ return ( - +
- + Date: Thu, 4 Apr 2024 01:03:44 -0700 Subject: [PATCH 061/158] Style: Fix styling --- .../AccessApprovalRequest.tsx | 133 +++++++++--------- 1 file changed, 70 insertions(+), 63 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 8ac0b5b78..b072513dc 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -15,7 +15,6 @@ import ms from "ms"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; -import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DropdownMenu, @@ -31,7 +30,6 @@ import { } from "@app/components/v2"; import { ProjectPermissionActions, - ProjectPermissionSub, useProjectPermission, useSubscription, useWorkspace @@ -53,7 +51,7 @@ const DisplayBadge = ({ text, className }: { text: string; className?: string }) return (
@@ -232,20 +230,29 @@ const SelectAccessModal = ({ ); }; -const generateRequestText = (request: TAccessApprovalRequest) => { +const generateRequestText = (request: TAccessApprovalRequest, membershipId: string) => { const { isTemporary } = request; return ( - - Requested {isTemporary ? "temporary" : "permanent"} access to{" "} - - {request.policy.secretPath} - - in - - {request.environmentName} - - +
+
+ Requested {isTemporary ? "temporary" : "permanent"} access to{" "} + + {request.policy.secretPath} + + in + + {request.environmentName} + +
+
+ {request.requestedBy === membershipId && ( + + + + )} +
+
); }; @@ -265,7 +272,7 @@ export const AccessApprovalRequest = ({ "reviewRequest", "upgradePlan" ] as const); - const { permission, membership } = useProjectPermission(); + const { membership } = useProjectPermission(); const { subscription } = useSubscription(); const { currentWorkspace } = useWorkspace(); @@ -284,12 +291,7 @@ export const AccessApprovalRequest = ({ }); const { data: policies, isLoading: policiesLoading } = useGetAccessApprovalPolicies({ - projectSlug, - options: { - enabled: - permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) && - !!projectSlug - } + projectSlug }); const { data: requests } = useGetAccessApprovalRequests({ @@ -371,31 +373,24 @@ export const AccessApprovalRequest = ({
- - {(isAllowed) => ( - - - - )} - + +
@@ -506,10 +501,11 @@ export const AccessApprovalRequest = ({
{ + if (!details.isApprover) return; if (details.isReviewedByUser || details.isRejectedByAnyone) return; setSelectedRequest({ @@ -519,6 +515,8 @@ export const AccessApprovalRequest = ({ handlePopUpOpen("reviewRequest"); }} onKeyDown={(evt) => { + if (!details.isApprover) return; + if (details.isReviewedByUser || details.isRejectedByAnyone) return; if (evt.key === "Enter") { setSelectedRequest({ ...request, @@ -528,24 +526,33 @@ export const AccessApprovalRequest = ({ } }} > -
-
- - {generateRequestText(request)} +
+
+
+ + {generateRequestText(request, membership.id)} +
+
+
+ {membersGroupById?.[request.requestedBy]?.user && ( + <> + Requested {formatDistance(new Date(request.createdAt), new Date())}{" "} + ago by {membersGroupById?.[request.requestedBy]?.user?.firstName}{" "} + {membersGroupById?.[request.requestedBy]?.user?.lastName} ( + {membersGroupById?.[request.requestedBy]?.user?.email}){" "} + + )} +
+
+ {details.isApprover && ( + + )} +
+
- - Requested {formatDistance(new Date(request.createdAt), new Date())} ago by{" "} - {membersGroupById?.[request.requestedBy]?.user?.firstName}{" "} - {membersGroupById?.[request.requestedBy]?.user?.lastName} ( - {membersGroupById?.[request.requestedBy]?.user?.email}){" "} - - - {details.isApprover && ( - - )}
); From da5278f6bf1f77b4233d8a8e5f959c0cf0204aad Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:03:58 -0700 Subject: [PATCH 062/158] Fix: Rename change -> secret --- frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx | 4 ++-- .../components/SecretApprovalRequestAction.tsx | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index a12881793..18ce5f391 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -47,8 +47,8 @@ export const SecretApprovalPage = () => {
- Change Requests - Change Request Policies + Secret Requests + Secret Policies Access Requests Access Request Policies diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx index 51f26a549..c3d804779 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx @@ -37,7 +37,6 @@ export const SecretApprovalRequestAction = ({ workspaceId, canApprove }: Props) => { - const { mutateAsync: performSecretApprovalMerge, isLoading: isMerging } = usePerformSecretApprovalRequestMerge(); @@ -136,7 +135,7 @@ export const SecretApprovalRequestAction = ({
- Change request merged + Secret approval merged Merged by {statusChangeByEmail} @@ -150,7 +149,7 @@ export const SecretApprovalRequestAction = ({
- Change request has been closed + Secret approval has been closed Closed by {statusChangeByEmail} From 32a110e0ca8487946ee240a24461014778ae868d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 12:44:49 -0700 Subject: [PATCH 063/158] Fix: Multiple approvers acceptance bug --- .../access-approval-request-service.ts | 40 +++++++++++-------- .../AccessApprovalRequest.tsx | 32 ++++++++++----- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 0c170c40f..b7b61fd8e 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -274,8 +274,8 @@ export const accessApprovalRequestServiceFactory = ({ const approvedReviews = allReviews.filter((r) => r.status === ApprovalStatus.APPROVED); - // If all approvers have approved the request, update the privilege to approved - if (approvedReviews.length === policy.approvers.length) { + // approvals is the required number of approvals. If the number of approved reviews is equal to the number of required approvals, then the request is approved. + if (approvedReviews.length === policy.approvals) { if (accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) { throw new BadRequestError({ message: "Temporary range is required for temporary access" }); } @@ -284,27 +284,33 @@ export const accessApprovalRequestServiceFactory = ({ if (!accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) { // Permanent access - const privilege = await additionalPrivilegeDAL.create({ - projectMembershipId: accessApprovalRequest.requestedBy, - slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, - permissions: JSON.stringify(accessApprovalRequest.permissions) - }); + const privilege = await additionalPrivilegeDAL.create( + { + projectMembershipId: accessApprovalRequest.requestedBy, + slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, + permissions: JSON.stringify(accessApprovalRequest.permissions) + }, + tx + ); privilegeId = privilege.id; } else { // Temporary access const relativeTempAllocatedTimeInMs = ms(accessApprovalRequest.temporaryRange!); const startTime = new Date(); - const privilege = await additionalPrivilegeDAL.create({ - projectMembershipId: accessApprovalRequest.requestedBy, - slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, - permissions: JSON.stringify(accessApprovalRequest.permissions), - isTemporary: true, - temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, - temporaryRange: accessApprovalRequest.temporaryRange!, - temporaryAccessStartTime: startTime, - temporaryAccessEndTime: new Date(new Date(startTime).getTime() + relativeTempAllocatedTimeInMs) - }); + const privilege = await additionalPrivilegeDAL.create( + { + projectMembershipId: accessApprovalRequest.requestedBy, + slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, + permissions: JSON.stringify(accessApprovalRequest.permissions), + isTemporary: true, + temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, + temporaryRange: accessApprovalRequest.temporaryRange!, + temporaryAccessStartTime: startTime, + temporaryAccessEndTime: new Date(new Date(startTime).getTime() + relativeTempAllocatedTimeInMs) + }, + tx + ); privilegeId = privilege.id; } diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index b072513dc..2421a629d 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -342,12 +342,14 @@ export const AccessApprovalRequest = ({ displayData = { label: "Access Granted", colorClass: "bg-green/20 text-green" }; else if (isRejectedByAnyone) displayData = { label: "Rejected", colorClass: "bg-red/20 text-red" }; - else if (userReviewStatus === ApprovalStatus.APPROVED) + else if (userReviewStatus === ApprovalStatus.APPROVED) { displayData = { - label: `Pending ${request.policy.approvals - request.reviewers.length} reviews`, + label: `Pending ${request.policy.approvals - request.reviewers.length} review${ + request.policy.approvals - request.reviewers.length > 1 ? "s" : "" + }`, colorClass: "bg-yellow/20 text-yellow" }; - else if (!isReviewedByUser) + } else if (!isReviewedByUser) displayData = { label: "Review Required", colorClass: "bg-yellow/20 text-yellow" @@ -499,14 +501,21 @@ export const AccessApprovalRequest = ({ return (
{ - if (!details.isApprover) return; - if (details.isReviewedByUser || details.isRejectedByAnyone) return; + if ( + !details.isApprover || + details.isReviewedByUser || + details.isRejectedByAnyone || + details.isAccepted + ) + return; setSelectedRequest({ ...request, @@ -515,8 +524,13 @@ export const AccessApprovalRequest = ({ handlePopUpOpen("reviewRequest"); }} onKeyDown={(evt) => { - if (!details.isApprover) return; - if (details.isReviewedByUser || details.isRejectedByAnyone) return; + if ( + !details.isApprover || + details.isAccepted || + details.isReviewedByUser || + details.isRejectedByAnyone + ) + return; if (evt.key === "Enter") { setSelectedRequest({ ...request, @@ -543,7 +557,7 @@ export const AccessApprovalRequest = ({ )}
-
+
{details.isApprover && ( Date: Fri, 29 Mar 2024 16:24:15 +0100 Subject: [PATCH 064/158] Draft --- backend/src/ee/services/license/licence-fns.ts | 2 +- .../src/components/permissions/PermissionDeniedBanner.tsx | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 189a3c4e0..492d09ec3 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ status: null, trial_end: null, has_used_trial: true, - secretApproval: false, + secretApproval: true, secretRotation: true }); diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx index b3c7a4f53..f9707d634 100644 --- a/frontend/src/components/permissions/PermissionDeniedBanner.tsx +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -3,6 +3,8 @@ import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; +import { Button } from "../v2"; + type Props = { containerClassName?: string; className?: string; @@ -32,6 +34,9 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children )}
+
); From 4326ce970a2093e9e28fc2791048ff3a974e0dd3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 1 Apr 2024 09:49:22 -0700 Subject: [PATCH 065/158] Feat: Request access --- backend/scripts/generate-schema-types.ts | 2 +- .../db/migrations/20240330075122_access-approval-policy.ts | 1 + .../access-approval-policy/access-approval-policy-service.ts | 4 ---- frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx | 4 ++++ .../AccessApprovalPolicyList/AccessApprovalPolicyList.tsx | 2 +- .../AccessApprovalPolicyList/components/AccessPolicyForm.tsx | 3 --- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 8c913991f..28b736152 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -5,7 +5,7 @@ import knex from "knex"; import { writeFileSync } from "fs"; dotenv.config({ - path: path.join(__dirname, "../../.env.migration") + path: path.join(__dirname, "../../.env") }); const db = knex({ diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index 8203fb333..20a50c37f 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -34,6 +34,7 @@ export async function up(knex: Knex): Promise { export async function down(knex: Knex): Promise { await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover); await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); + await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); } diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index d46f93e1d..f08b94122 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -155,7 +155,6 @@ export const accessApprovalPolicyServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { @@ -179,7 +178,6 @@ export const accessApprovalPolicyServiceFactory = ({ ); await verifyApprovers({ - projectId: accessApprovalPolicy.projectId, orgId: actorOrgId, envSlug: accessApprovalPolicy.environment.slug, secretPath: doc.secretPath!, @@ -189,8 +187,6 @@ export const accessApprovalPolicyServiceFactory = ({ }); if (secretApprovers.length !== approvers.length) - throw new BadRequestError({ message: "Approver not found in project" }); - if (doc.approvals > secretApprovers.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx); await accessApprovalPolicyApproverDAL.insertMany( diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 18ce5f391..98ed330f5 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -67,6 +67,10 @@ export const SecretApprovalPage = () => { + + + +
); diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx index ba12c5189..3e8fdb3b9 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx @@ -83,7 +83,7 @@ export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => { return (
-
+
Access Request Policies
diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index dd51ea53d..081556041 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -177,7 +177,6 @@ export const AccessPolicyForm = ({ /> ( @@ -187,8 +186,6 @@ export const AccessPolicyForm = ({ /> ( Date: Wed, 3 Apr 2024 16:35:51 -0700 Subject: [PATCH 066/158] Feat: Request Access (migrations) --- .../src/db/migrations/20240330075122_access-approval-policy.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index 20a50c37f..8203fb333 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -34,7 +34,6 @@ export async function up(knex: Knex): Promise { export async function down(knex: Knex): Promise { await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover); await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); - await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); } From 6d9de752d713673ab77605c1b575d66eec01dca1 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:37:49 -0700 Subject: [PATCH 067/158] Feat: Request access (new routes) --- backend/src/ee/routes/v1/access-approval-request-router.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 4b173cfa7..104f32665 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -2,10 +2,6 @@ import { z } from "zod"; import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas"; import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; -import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AuthMode } from "@app/services/auth/auth-type"; - -export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => { server.route({ url: "/", method: "POST", From 5e1484bd05c86f6010da496e21112f72e4505395 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:39:15 -0700 Subject: [PATCH 068/158] Fix: Validate approvers access --- .../access-approval-policy/access-approval-policy-service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index f08b94122..cca29e431 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -155,6 +155,7 @@ export const accessApprovalPolicyServiceFactory = ({ actorAuthMethod, actorOrgId ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { From e48377dea9e2f116574f527942269dc40d54a492 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:43:48 -0700 Subject: [PATCH 069/158] Fix: Remove redundant code --- .../src/components/permissions/PermissionDeniedBanner.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx index f9707d634..b3c7a4f53 100644 --- a/frontend/src/components/permissions/PermissionDeniedBanner.tsx +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -3,8 +3,6 @@ import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { Button } from "../v2"; - type Props = { containerClassName?: string; className?: string; @@ -34,9 +32,6 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children )}
-
); From 926d324ae33d8f9ea6aab493ab4902d42e2bdd5d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:49:00 -0700 Subject: [PATCH 070/158] Fix: Added support for request access --- .../SpecificPrivilegeSection.tsx | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx index 88dba72a1..4a7e45662 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -311,6 +311,91 @@ export const SpecificPrivilegeSecretForm = ({ )}`; } return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); + }; + + }; + + // This is used for requesting access additional privileges, not directly creating a privilege! + const handleRequestAccess = async (data: TSecretPermissionForm) => { + if (!policies) return; + if (!currentWorkspace) { + createNotification({ + type: "error", + text: "No workspace found.", + title: "Error" + }); + return; + } + + if (!data.secretPath) { + createNotification({ + type: "error", + text: "Please select a secret path", + title: "Error" + }); + return; + } + + const actions = [ + { action: ProjectPermissionActions.Read, allowed: data.read }, + { action: ProjectPermissionActions.Create, allowed: data.create }, + { action: ProjectPermissionActions.Delete, allowed: data.delete }, + { action: ProjectPermissionActions.Edit, allowed: data.edit } + ]; + const conditions: Record = { environment: data.environmentSlug }; + if (data.secretPath) { + conditions.secretPath = { $glob: data.secretPath }; + } + await requestAccess.mutateAsync({ + ...data, + ...(data.temporaryAccess.isTemporary && { + temporaryAccessStartTime: data.temporaryAccess.temporaryAccessStartTime, + temporaryAccessEndTime: data.temporaryAccess.temporaryAccessEndTime, + temporaryRange: data.temporaryAccess.temporaryRange, + temporaryMode: "relative" + }), + envSlug: data.environmentSlug, + secretPath: data.secretPath, + projectSlug: currentWorkspace.slug, + projectMembershipId: projectMembership.id, + isTemporary: data.temporaryAccess.isTemporary, + permissions: actions + .filter(({ allowed }) => allowed) + .map(({ action }) => ({ + action, + subject: [ProjectPermissionSub.Secrets], + conditions + })) + }); + + createNotification({ + type: "success", + text: "Successfully requested access" + }); + privilegeForm.reset(); + if (onClose) onClose(); + }; + + const handleSubmit = async (data: TSecretPermissionForm) => { + if (privilege) { + handleUpdatePrivilege(data); + } else { + handleRequestAccess(data); + } + }; + + const getAccessLabel = (exactTime = false) => { + if (isExpired) return "Access expired"; + if (!temporaryAccessField?.isTemporary) return "Permanent"; + + if (exactTime) + return `Until ${format( + new Date(temporaryAccessField.temporaryAccessEndTime || ""), + "yyyy-MM-dd HH:mm:ss" + )}`; + return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); + }; + }; return ( From 70c06c91c8d489003c428cbf4609b57f13e99d26 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:51:21 -0700 Subject: [PATCH 071/158] Update SecretApprovalPage.tsx --- frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 98ed330f5..5bd6ecacf 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -69,7 +69,7 @@ export const SecretApprovalPage = () => { - +
From 5a04371fb0e0f13c6324d9e4fb66b84a2f499aeb Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:53:02 -0700 Subject: [PATCH 072/158] Update generate-schema-types.ts --- backend/scripts/generate-schema-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 28b736152..8c913991f 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -5,7 +5,7 @@ import knex from "knex"; import { writeFileSync } from "fs"; dotenv.config({ - path: path.join(__dirname, "../../.env") + path: path.join(__dirname, "../../.env.migration") }); const db = knex({ From 0aa77f90c80df578abb41d2bae50fc24de062681 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:13:23 -0700 Subject: [PATCH 073/158] Update SpecificPrivilegeSection.tsx --- .../MemberRoleForm/SpecificPrivilegeSection.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx index 4a7e45662..b47768d1f 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -388,14 +388,13 @@ export const SpecificPrivilegeSecretForm = ({ if (isExpired) return "Access expired"; if (!temporaryAccessField?.isTemporary) return "Permanent"; - if (exactTime) + if (exactTime) { return `Until ${format( new Date(temporaryAccessField.temporaryAccessEndTime || ""), "yyyy-MM-dd HH:mm:ss" )}`; + } return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); - }; - }; return ( From a687b1d0db278c7346bbcaf7c0c35916bf700f2a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:43:45 -0700 Subject: [PATCH 074/158] Update licence-fns.ts --- backend/src/ee/services/license/licence-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 492d09ec3..189a3c4e0 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ status: null, trial_end: null, has_used_trial: true, - secretApproval: true, + secretApproval: false, secretRotation: true }); From bb9503471f72ab4cb8bee57157e83ea5664670dd Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 3 Apr 2024 20:24:33 -0700 Subject: [PATCH 075/158] style changes --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 2421a629d..963dce37c 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -496,7 +496,7 @@ export const AccessApprovalRequest = ({
)} {!!filteredRequests?.length && - filteredRequests?.map((request) => { + requests?.map((request) => { const details = generateRequestDetails(request); return ( From 22470376d901d778ef8190dfa3c2b25fd82d0e8f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:29:20 -0700 Subject: [PATCH 076/158] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 963dce37c..2421a629d 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -496,7 +496,7 @@ export const AccessApprovalRequest = ({
)} {!!filteredRequests?.length && - requests?.map((request) => { + filteredRequests?.map((request) => { const details = generateRequestDetails(request); return ( From bc810ea567d6cf3c0d9b4d0abfbb3027bb6c37d1 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:33:58 -0700 Subject: [PATCH 077/158] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 2421a629d..960bdc8b4 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -490,7 +490,7 @@ export const AccessApprovalRequest = ({
- {filteredRequests?.length === 0 && ( + {filteredRequests?.length === 0 && ( //
From 8e82bfae868b27b38b5b0b88c2475df3aa9a09fc Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:34:11 -0700 Subject: [PATCH 078/158] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 960bdc8b4..2421a629d 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -490,7 +490,7 @@ export const AccessApprovalRequest = ({
- {filteredRequests?.length === 0 && ( // + {filteredRequests?.length === 0 && (
From dab69dcb517f294aa3040560e72c2d89f63e97f7 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:03:19 -0700 Subject: [PATCH 079/158] Removed unnessecary types --- .../MemberRoleForm/SpecificPrivilegeSection.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx index b47768d1f..4dc7437c4 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -349,15 +349,9 @@ export const SpecificPrivilegeSecretForm = ({ await requestAccess.mutateAsync({ ...data, ...(data.temporaryAccess.isTemporary && { - temporaryAccessStartTime: data.temporaryAccess.temporaryAccessStartTime, - temporaryAccessEndTime: data.temporaryAccess.temporaryAccessEndTime, - temporaryRange: data.temporaryAccess.temporaryRange, - temporaryMode: "relative" + temporaryRange: data.temporaryAccess.temporaryRange }), - envSlug: data.environmentSlug, - secretPath: data.secretPath, projectSlug: currentWorkspace.slug, - projectMembershipId: projectMembership.id, isTemporary: data.temporaryAccess.isTemporary, permissions: actions .filter(({ allowed }) => allowed) @@ -388,7 +382,7 @@ export const SpecificPrivilegeSecretForm = ({ if (isExpired) return "Access expired"; if (!temporaryAccessField?.isTemporary) return "Permanent"; - if (exactTime) { + if (exactTime && !policies) { return `Until ${format( new Date(temporaryAccessField.temporaryAccessEndTime || ""), "yyyy-MM-dd HH:mm:ss" From c874c943c19c21c9a6f605088281de4e2e733bf0 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 20:14:02 -0700 Subject: [PATCH 080/158] Fix: Rebase errors --- .../v1/access-approval-request-router.ts | 4 + .../access-approval-policy-service.ts | 1 + .../SpecificPrivilegeSection.tsx | 77 ------------------- .../components/AccessPolicyForm.tsx | 2 + 4 files changed, 7 insertions(+), 77 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 104f32665..4b173cfa7 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -2,6 +2,10 @@ import { z } from "zod"; import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas"; import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => { server.route({ url: "/", method: "POST", diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index cca29e431..c384dd71a 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -179,6 +179,7 @@ export const accessApprovalPolicyServiceFactory = ({ ); await verifyApprovers({ + projectId: accessApprovalPolicy.projectId, orgId: actorOrgId, envSlug: accessApprovalPolicy.environment.slug, secretPath: doc.secretPath!, diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx index 4dc7437c4..2f59e553e 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -238,83 +238,6 @@ export const SpecificPrivilegeSecretForm = ({ } }; - // This is used for requesting access additional privileges, not directly creating a privilege! - const handleRequestAccess = async (data: TSecretPermissionForm) => { - if (!policies) return; - if (!currentWorkspace) { - createNotification({ - type: "error", - text: "No workspace found.", - title: "Error" - }); - return; - } - - if (!data.secretPath) { - createNotification({ - type: "error", - text: "Please select a secret path", - title: "Error" - }); - return; - } - - const actions = [ - { action: ProjectPermissionActions.Read, allowed: data.read }, - { action: ProjectPermissionActions.Create, allowed: data.create }, - { action: ProjectPermissionActions.Delete, allowed: data.delete }, - { action: ProjectPermissionActions.Edit, allowed: data.edit } - ]; - const conditions: Record = { environment: data.environmentSlug }; - if (data.secretPath) { - conditions.secretPath = { $glob: data.secretPath }; - } - await requestAccess.mutateAsync({ - ...(data.temporaryAccess.isTemporary && { - temporaryRange: data.temporaryAccess.temporaryRange - }), - projectSlug: currentWorkspace.slug, - isTemporary: data.temporaryAccess.isTemporary, - permissions: actions - .filter(({ allowed }) => allowed) - .map(({ action }) => ({ - action, - subject: [ProjectPermissionSub.Secrets], - conditions - })) - }); - - createNotification({ - type: "success", - text: "Successfully requested access" - }); - privilegeForm.reset(); - if (onClose) onClose(); - }; - - const handleSubmit = async (data: TSecretPermissionForm) => { - if (privilege) { - handleUpdatePrivilege(data); - } else { - handleRequestAccess(data); - } - }; - - const getAccessLabel = (exactTime = false) => { - if (isExpired) return "Access expired"; - if (!temporaryAccessField?.isTemporary) return "Permanent"; - - if (exactTime && !policies) { - return `Until ${format( - new Date(temporaryAccessField.temporaryAccessEndTime || ""), - "yyyy-MM-dd HH:mm:ss" - )}`; - } - return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); - }; - - }; - // This is used for requesting access additional privileges, not directly creating a privilege! const handleRequestAccess = async (data: TSecretPermissionForm) => { if (!policies) return; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index 081556041..a07acb584 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -186,6 +186,8 @@ export const AccessPolicyForm = ({ /> ( Date: Thu, 4 Apr 2024 21:00:02 -0700 Subject: [PATCH 081/158] Update SecretApprovalPage.tsx --- frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 5bd6ecacf..d66a6c8cc 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -63,11 +63,6 @@ export const SecretApprovalPage = () => { - - - - - From 01e7ed23bae8935537d09e27ded9280be53ffa4f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 21:12:05 -0700 Subject: [PATCH 082/158] Fixed bugs --- ....ts => 20240330075120_org-memberships-unique-constraint.ts} | 0 .../AccessApprovalPolicyList/components/AccessPolicyForm.tsx | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) rename backend/src/db/migrations/{20240405000045_org-memberships-unique-constraint.ts => 20240330075120_org-memberships-unique-constraint.ts} (100%) diff --git a/backend/src/db/migrations/20240405000045_org-memberships-unique-constraint.ts b/backend/src/db/migrations/20240330075120_org-memberships-unique-constraint.ts similarity index 100% rename from backend/src/db/migrations/20240405000045_org-memberships-unique-constraint.ts rename to backend/src/db/migrations/20240330075120_org-memberships-unique-constraint.ts diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index a07acb584..dd51ea53d 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -177,6 +177,7 @@ export const AccessPolicyForm = ({ /> ( @@ -186,8 +187,8 @@ export const AccessPolicyForm = ({ /> ( Date: Thu, 4 Apr 2024 22:14:46 -0700 Subject: [PATCH 083/158] Migration improvements --- .../20240330075122_access-approval-policy.ts | 17 ++++++++++++----- .../20240401173320_access_approval_requests.ts | 11 +++++++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index 8203fb333..79af3aa6e 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -14,8 +14,8 @@ export async function up(knex: Knex): Promise { t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); t.timestamps(true, true, true); }); + await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); } - await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); if (!(await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover))) { await knex.schema.createTable(TableName.AccessApprovalPolicyApprover, (t) => { @@ -26,14 +26,21 @@ export async function up(knex: Knex): Promise { t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE"); t.timestamps(true, true, true); }); + await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); } - - await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); } export async function down(knex: Knex): Promise { + const approverTableExists = await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover); + const policyTableExists = await knex.schema.hasTable(TableName.AccessApprovalPolicy); + await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover); await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); - await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); - await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); + + if (approverTableExists) { + await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); + } + if (policyTableExists) { + await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); + } } diff --git a/backend/src/db/migrations/20240401173320_access_approval_requests.ts b/backend/src/db/migrations/20240401173320_access_approval_requests.ts index 901be9a78..c03287b57 100644 --- a/backend/src/db/migrations/20240401173320_access_approval_requests.ts +++ b/backend/src/db/migrations/20240401173320_access_approval_requests.ts @@ -43,9 +43,16 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { + const reviewerTableExists = await knex.schema.hasTable(TableName.AccessApprovalRequestReviewer); + const requestTableExists = await knex.schema.hasTable(TableName.AccessApprovalRequest); + await knex.schema.dropTableIfExists(TableName.AccessApprovalRequestReviewer); await knex.schema.dropTableIfExists(TableName.AccessApprovalRequest); - await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); - await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); + if (reviewerTableExists) { + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); + } + if (requestTableExists) { + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); + } } From 1785548a40049e84bb94e4ea396cdd889d46750e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:55:54 -0700 Subject: [PATCH 084/158] Fix: Sort by createdAt --- .../access-approval-request/access-approval-request-dal.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts index 78fef7c8a..c3f4c72a6 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts @@ -81,7 +81,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { .as("privilegeTemporaryAccessEndTime"), db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegePermissions") - ); + ) + .orderBy(`${TableName.AccessApprovalRequest}.createdAt`, "desc"); const formattedDocs = sqlNestRelationships({ data: docs, From c7d2dfd351abccb027a2131bc447b360b7fbccaf Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:57:14 -0700 Subject: [PATCH 085/158] Fix: Requesting approvals on previously rejected resources --- .../access-approval-request-service.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index b7b61fd8e..d9762d879 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -128,7 +128,15 @@ export const accessApprovalRequestServiceFactory = ({ throw new BadRequestError({ message: "You already have an active privilege with the same criteria" }); } } else { - throw new BadRequestError({ message: "You already have a pending access request with the same criteria" }); + const reviewers = await accessApprovalRequestReviewerDAL.find({ + requestId: duplicateRequest.id + }); + + const isRejected = reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED); + + if (!isRejected) { + throw new BadRequestError({ message: "You already have a pending access request with the same criteria" }); + } } } From 44370d49e320284f6210956fc7945c34abba44a6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:57:36 -0700 Subject: [PATCH 086/158] Fix: Add tooltip for clarity and fix wording --- .../SpecificPrivilegeSection.tsx | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx index 2f59e553e..795ccbb24 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -344,20 +344,27 @@ export const SpecificPrivilegeSecretForm = ({ render={({ field }) => { if (policies) { return ( - - - + +
+ + + +
+
); } return ( @@ -515,8 +522,9 @@ export const SpecificPrivilegeSecretForm = ({ ); }} > - {temporaryAccessField.isTemporary ? "Restart" : "Grant"} + {temporaryAccessField.isTemporary && !policies ? "Restart" : "Grant"} + {temporaryAccessField.isTemporary && ( )}
From c911a7cd8115c820e21831f0ff08a3416f2e24df Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:57:59 -0700 Subject: [PATCH 087/158] Fix: Don't display requested by when user has no access to read workspace members --- .../AccessApprovalRequest.tsx | 57 ++++++++++--------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 2421a629d..b6d69d0c9 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -30,6 +30,7 @@ import { } from "@app/components/v2"; import { ProjectPermissionActions, + ProjectPermissionSub, useProjectPermission, useSubscription, useWorkspace @@ -272,7 +273,7 @@ export const AccessApprovalRequest = ({ "reviewRequest", "upgradePlan" ] as const); - const { membership } = useProjectPermission(); + const { membership, permission } = useProjectPermission(); const { subscription } = useSubscription(); const { currentWorkspace } = useWorkspace(); @@ -460,33 +461,37 @@ export const AccessApprovalRequest = ({ ))} - - - - - - Select an author - {members?.map(({ user, id }) => ( - - setRequestedByFilter((state) => (state === id ? undefined : id)) + {!!permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Member) && ( + + + + + + Select an author + {members?.map(({ user, id }) => ( + + setRequestedByFilter((state) => (state === id ? undefined : id)) + } + key={`request-filter-member-${id}`} + icon={requestedByFilter === id && } + iconPos="right" + > + {user.email} + + ))} + + + )}
From 4e449f62c0e6bd7c8552d2aa4922153dd53df54c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:58:16 -0700 Subject: [PATCH 088/158] Fix: Don't display requested by when user has no access to read workspace members --- .../SecretApprovalRequest.tsx | 63 +++++++++++-------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx index 96976d140..32a7ff6de 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx @@ -19,7 +19,13 @@ import { EmptyState, Skeleton } from "@app/components/v2"; -import { useUser, useWorkspace } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useUser, + useWorkspace +} from "@app/context"; import { useGetSecretApprovalRequestCount, useGetSecretApprovalRequests, @@ -58,6 +64,7 @@ export const SecretApprovalRequest = () => { const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } = useGetSecretApprovalRequestCount({ workspaceId }); const { user: presentUser } = useUser(); + const { permission } = useProjectPermission(); const { data: members } = useGetWorkspaceUsers(workspaceId); const membersGroupById = members?.reduce>( (prev, curr) => ({ ...prev, [curr.id]: curr }), @@ -156,31 +163,37 @@ export const SecretApprovalRequest = () => { ))} - - - - - - Select an author - {members?.map(({ user, id }) => ( - setCommitterFilter((state) => (state === id ? undefined : id))} - key={`request-filter-member-${id}`} - icon={committerFilter === id && } - iconPos="right" + {!!permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Member) && ( + + + + + + Select an author + {members?.map(({ user, id }) => ( + + setCommitterFilter((state) => (state === id ? undefined : id)) + } + key={`request-filter-member-${id}`} + icon={committerFilter === id && } + iconPos="right" + > + {user.email} + + ))} + + + )}
From dc3014409f39e8896d2fd466ed7e41b1dcaf916e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:59:01 -0700 Subject: [PATCH 089/158] Delete access-approval-request-secret-dal.ts --- .../access-approval-request-secret-dal.ts | 230 ------------------ 1 file changed, 230 deletions(-) delete mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts deleted file mode 100644 index d458d58ff..000000000 --- a/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { Knex } from "knex"; - -import { TDbClient } from "@app/db"; -import { - SecretApprovalRequestsSecretsSchema, - TableName, - TSecretApprovalRequestsSecrets, - TSecretTags -} from "@app/db/schemas"; -import { BadRequestError, DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; - -export type TAccessApprovalRequestSecretDALFactory = ReturnType; - -export const accessApprovalRequestSecretDALFactory = (db: TDbClient) => { - const accessApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret); - const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag); - - const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => { - try { - const existingApprovalSecrets = await accessApprovalRequestSecretOrm.find( - { - $in: { - id: data.map((el) => el.id) - } - }, - { tx } - ); - - if (existingApprovalSecrets.length !== data.length) { - throw new BadRequestError({ message: "Some of the secret approvals do not exist" }); - } - - if (data.length === 0) return []; - - const updatedApprovalSecrets = await (tx || db)(TableName.SecretApprovalRequestSecret) - .insert(data) - .onConflict("id") // this will cause a conflict then merge the data - .merge() // Merge the data with the existing data - .returning("*"); - - return updatedApprovalSecrets; - } catch (error) { - throw new DatabaseError({ error, name: "bulk update secret" }); - } - }; - - const findByRequestId = async (requestId: string, tx?: Knex) => { - try { - const doc = await (tx || db)({ - secVerTag: TableName.SecretTag - }) - .from(TableName.SecretApprovalRequestSecret) - .where({ requestId }) - .leftJoin( - TableName.SecretApprovalRequestSecretTag, - `${TableName.SecretApprovalRequestSecret}.id`, - `${TableName.SecretApprovalRequestSecretTag}.secretId` - ) - .leftJoin(TableName.SecretTag, `${TableName.SecretApprovalRequestSecretTag}.tagId`, `${TableName.SecretTag}.id`) - .leftJoin(TableName.Secret, `${TableName.SecretApprovalRequestSecret}.secretId`, `${TableName.Secret}.id`) - .leftJoin( - TableName.SecretVersion, - `${TableName.SecretVersion}.id`, - `${TableName.SecretApprovalRequestSecret}.secretVersion` - ) - .leftJoin( - TableName.SecretVersionTag, - `${TableName.SecretVersionTag}.${TableName.SecretVersion}Id`, - `${TableName.SecretVersion}.id` - ) - .leftJoin( - db.ref(TableName.SecretTag).as("secVerTag"), - `${TableName.SecretVersionTag}.${TableName.SecretTag}Id`, - db.ref("id").withSchema("secVerTag") - ) - .select(selectAllTableCols(TableName.SecretApprovalRequestSecret)) - .select({ - secVerTagId: "secVerTag.id", - secVerTagColor: "secVerTag.color", - secVerTagSlug: "secVerTag.slug", - secVerTagName: "secVerTag.name" - }) - .select( - db.ref("id").withSchema(TableName.SecretTag).as("tagId"), - db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"), - db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), - db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), - db.ref("name").withSchema(TableName.SecretTag).as("tagName") - ) - .select( - db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"), - db.ref("version").withSchema(TableName.Secret).as("orgSecVersion"), - db.ref("secretKeyIV").withSchema(TableName.Secret).as("orgSecKeyIV"), - db.ref("secretKeyTag").withSchema(TableName.Secret).as("orgSecKeyTag"), - db.ref("secretKeyCiphertext").withSchema(TableName.Secret).as("orgSecKeyCiphertext"), - db.ref("secretValueIV").withSchema(TableName.Secret).as("orgSecValueIV"), - db.ref("secretValueTag").withSchema(TableName.Secret).as("orgSecValueTag"), - db.ref("secretValueCiphertext").withSchema(TableName.Secret).as("orgSecValueCiphertext"), - db.ref("secretCommentIV").withSchema(TableName.Secret).as("orgSecCommentIV"), - db.ref("secretCommentTag").withSchema(TableName.Secret).as("orgSecCommentTag"), - db.ref("secretCommentCiphertext").withSchema(TableName.Secret).as("orgSecCommentCiphertext") - ) - .select( - db.ref("version").withSchema(TableName.SecretVersion).as("secVerVersion"), - db.ref("secretKeyIV").withSchema(TableName.SecretVersion).as("secVerKeyIV"), - db.ref("secretKeyTag").withSchema(TableName.SecretVersion).as("secVerKeyTag"), - db.ref("secretKeyCiphertext").withSchema(TableName.SecretVersion).as("secVerKeyCiphertext"), - db.ref("secretValueIV").withSchema(TableName.SecretVersion).as("secVerValueIV"), - db.ref("secretValueTag").withSchema(TableName.SecretVersion).as("secVerValueTag"), - db.ref("secretValueCiphertext").withSchema(TableName.SecretVersion).as("secVerValueCiphertext"), - db.ref("secretCommentIV").withSchema(TableName.SecretVersion).as("secVerCommentIV"), - db.ref("secretCommentTag").withSchema(TableName.SecretVersion).as("secVerCommentTag"), - db.ref("secretCommentCiphertext").withSchema(TableName.SecretVersion).as("secVerCommentCiphertext") - ); - const formatedDoc = sqlNestRelationships({ - data: doc, - key: "id", - parentMapper: (data) => SecretApprovalRequestsSecretsSchema.omit({ secretVersion: true }).parse(data), - childrenMapper: [ - { - key: "tagJnId", - label: "tags" as const, - mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({ - id, - name, - slug, - color - }) - }, - { - key: "secretId", - label: "secret" as const, - mapper: ({ - orgSecKeyIV, - orgSecKeyTag, - orgSecValueIV, - orgSecVersion, - orgSecValueTag, - orgSecCommentIV, - orgSecBlindIndex, - orgSecCommentTag, - orgSecKeyCiphertext, - orgSecValueCiphertext, - orgSecCommentCiphertext, - secretId - }) => - secretId - ? { - id: secretId, - version: orgSecVersion, - secretBlindIndex: orgSecBlindIndex, - secretKeyIV: orgSecKeyIV, - secretKeyTag: orgSecKeyTag, - secretKeyCiphertext: orgSecKeyCiphertext, - secretValueIV: orgSecValueIV, - secretValueTag: orgSecValueTag, - secretValueCiphertext: orgSecValueCiphertext, - secretCommentIV: orgSecCommentIV, - secretCommentTag: orgSecCommentTag, - secretCommentCiphertext: orgSecCommentCiphertext - } - : undefined - }, - { - key: "secretVersion", - label: "secretVersion" as const, - mapper: ({ - secVerCommentIV, - secVerCommentCiphertext, - secVerCommentTag, - secVerValueCiphertext, - secVerKeyIV, - secVerKeyTag, - secVerValueIV, - secretVersion, - secVerValueTag, - secVerKeyCiphertext, - secVerVersion - }) => - secretVersion - ? { - version: secVerVersion, - id: secretVersion, - secretKeyIV: secVerKeyIV, - secretKeyTag: secVerKeyTag, - secretKeyCiphertext: secVerKeyCiphertext, - secretValueIV: secVerValueIV, - secretValueTag: secVerValueTag, - secretValueCiphertext: secVerValueCiphertext, - secretCommentIV: secVerCommentIV, - secretCommentTag: secVerCommentTag, - secretCommentCiphertext: secVerCommentCiphertext - } - : undefined, - childrenMapper: [ - { - key: "secVerTagId", - label: "tags" as const, - mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({ - // eslint-disable-next-line - id, - // eslint-disable-next-line - name, - // eslint-disable-next-line - slug, - // eslint-disable-next-line - color - }) - } - ] - } - ] - }); - return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({ - ...el, - secret: secret?.[0], - secretVersion: secretVersion?.[0] - })); - } catch (error) { - throw new DatabaseError({ error, name: "FindByRequestId" }); - } - }; - return { - ...accessApprovalRequestSecretOrm, - findByRequestId, - bulkUpdateNoVersionIncrement, - insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany - }; -}; From 972ecc3e9213da0a2072b08c3b8b92ef5023ffa5 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:18:02 -0700 Subject: [PATCH 090/158] Fix: Improved migrations --- .../20240330075122_access-approval-policy.ts | 11 ++--------- .../20240401173320_access_approval_requests.ts | 11 ++--------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index 79af3aa6e..61cb9274c 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -31,16 +31,9 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { - const approverTableExists = await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover); - const policyTableExists = await knex.schema.hasTable(TableName.AccessApprovalPolicy); - await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover); await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); - if (approverTableExists) { - await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); - } - if (policyTableExists) { - await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); - } + await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); + await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); } diff --git a/backend/src/db/migrations/20240401173320_access_approval_requests.ts b/backend/src/db/migrations/20240401173320_access_approval_requests.ts index c03287b57..901be9a78 100644 --- a/backend/src/db/migrations/20240401173320_access_approval_requests.ts +++ b/backend/src/db/migrations/20240401173320_access_approval_requests.ts @@ -43,16 +43,9 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { - const reviewerTableExists = await knex.schema.hasTable(TableName.AccessApprovalRequestReviewer); - const requestTableExists = await knex.schema.hasTable(TableName.AccessApprovalRequest); - await knex.schema.dropTableIfExists(TableName.AccessApprovalRequestReviewer); await knex.schema.dropTableIfExists(TableName.AccessApprovalRequest); - if (reviewerTableExists) { - await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); - } - if (requestTableExists) { - await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); - } + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); } From 9d362b8597d49d8acbb51160893ba3b50ffe8acf Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:18:15 -0700 Subject: [PATCH 091/158] Chore: Cleaned up models --- backend/src/db/schemas/models.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index f401e822c..24c6b8914 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -50,20 +50,16 @@ export enum TableName { IdentityProjectMembershipRole = "identity_project_membership_role", IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege", ScimToken = "scim_tokens", - - // New tables so far AccessApprovalPolicy = "access_approval_policies", AccessApprovalPolicyApprover = "access_approval_policies_approvers", AccessApprovalRequest = "access_approval_requests", AccessApprovalRequestReviewer = "access_approval_requests_reviewers", - SecretApprovalPolicy = "secret_approval_policies", SecretApprovalPolicyApprover = "secret_approval_policies_approvers", SecretApprovalRequest = "secret_approval_requests", SecretApprovalRequestReviewer = "secret_approval_requests_reviewers", SecretApprovalRequestSecret = "secret_approval_requests_secrets", SecretApprovalRequestSecretTag = "secret_approval_request_secret_tags", - SecretRotation = "secret_rotations", SecretRotationOutput = "secret_rotation_outputs", SamlConfig = "saml_configs", From dd43268506957f0b85cc1df3671a5afd60efae6c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:18:45 -0700 Subject: [PATCH 092/158] Fix: Made API endpoints more REST compliant --- .../ee/routes/v1/access-approval-policy-router.ts | 10 +++++----- backend/src/ee/routes/v1/index.ts | 4 ++-- frontend/src/hooks/api/accessApproval/mutation.tsx | 10 +++++----- frontend/src/hooks/api/accessApproval/queries.tsx | 14 +++++++------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-policy-router.ts b/backend/src/ee/routes/v1/access-approval-policy-router.ts index 8a8090042..3b8949d3b 100644 --- a/backend/src/ee/routes/v1/access-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts @@ -59,7 +59,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const approvals = await server.services.accessApprovalPolicy.getAccessApprovalPolicyByProjectId({ + const approvals = await server.services.accessApprovalPolicy.getAccessApprovalPolicyByProjectSlug({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -71,7 +71,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi }); server.route({ - url: "/policy-count", + url: "/count", method: "GET", schema: { querystring: z.object({ @@ -80,14 +80,14 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi }), response: { 200: z.object({ - policyCount: z.number() + count: z.number() }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { policyCount } = await server.services.accessApprovalPolicy.getAccessPolicyCountByEnvSlug({ + const { count } = await server.services.accessApprovalPolicy.getAccessPolicyCountByEnvSlug({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -95,7 +95,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi actorOrgId: req.permission.orgId, envSlug: req.query.envSlug }); - return { policyCount }; + return { count }; } }); diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index c73ed24c5..fc5c0865d 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -43,8 +43,8 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { prefix: "/secret-rotation-providers" }); - await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals" }); - await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approval-requests" }); + await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals/policies" }); + await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approvals/requests" }); await server.register( async (dynamicSecretRouter) => { diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index 1bf3841ff..5f595c8a2 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -17,7 +17,7 @@ export const useCreateAccessApprovalPolicy = () => { return useMutation<{}, {}, TCreateAccessPolicyDTO>({ mutationFn: async ({ environment, projectSlug, approvals, approvers, name, secretPath }) => { - const { data } = await apiRequest.post("/api/v1/access-approvals", { + const { data } = await apiRequest.post("/api/v1/access-approvals/policies", { environment, projectSlug, approvals, @@ -38,7 +38,7 @@ export const useUpdateAccessApprovalPolicy = () => { return useMutation<{}, {}, TUpdateAccessPolicyDTO>({ mutationFn: async ({ id, approvers, approvals, name, secretPath }) => { - const { data } = await apiRequest.patch(`/api/v1/access-approvals/${id}`, { + const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, { approvals, approvers, secretPath, @@ -57,7 +57,7 @@ export const useDeleteAccessApprovalPolicy = () => { return useMutation<{}, {}, TDeleteSecretPolicyDTO>({ mutationFn: async ({ id }) => { - const { data } = await apiRequest.delete(`/api/v1/access-approvals/${id}`); + const { data } = await apiRequest.delete(`/api/v1/access-approvals/policies/${id}`); return data; }, onSuccess: (_, { projectSlug }) => { @@ -71,7 +71,7 @@ export const useCreateAccessRequest = () => { return useMutation<{}, {}, TCreateAccessRequestDTO>({ mutationFn: async ({ projectSlug, ...request }) => { const { data } = await apiRequest.post( - "/api/v1/access-approval-requests", + "/api/v1/access-approvals/requests", { ...request, permissions: request.permissions ? packRules(request.permissions) : undefined @@ -106,7 +106,7 @@ export const useReviewAccessRequest = () => { >({ mutationFn: async ({ requestId, status }) => { const { data } = await apiRequest.post( - `/api/v1/access-approval-requests/${requestId}/review`, + `/api/v1/access-approvals/requests/${requestId}/review`, { status } diff --git a/frontend/src/hooks/api/accessApproval/queries.tsx b/frontend/src/hooks/api/accessApproval/queries.tsx index 112c53fe3..599962e43 100644 --- a/frontend/src/hooks/api/accessApproval/queries.tsx +++ b/frontend/src/hooks/api/accessApproval/queries.tsx @@ -19,7 +19,7 @@ export const accessApprovalKeys = { [{ workspaceId, environment }, "access-approval-policy"] as const, getAccessApprovalRequests: (projectSlug: string, envSlug?: string, requestedBy?: string) => - [{ projectSlug, envSlug, requestedBy }, "access-approval-requests"] as const, + [{ projectSlug, envSlug, requestedBy }, "access-approvals-requests"] as const, getAccessApprovalRequestCount: (projectSlug: string) => [{ projectSlug }, "access-approval-request-count"] as const }; @@ -28,13 +28,13 @@ export const fetchPolicyApprovalCount = async ({ projectSlug, envSlug }: TGetAccessPolicyApprovalCountDTO) => { - const { data } = await apiRequest.get<{ policyCount: number }>( - "/api/v1/access-approvals/policy-count", + const { data } = await apiRequest.get<{ count: number }>( + "/api/v1/access-approvals/policies/count", { params: { projectSlug, envSlug } } ); - return data.policyCount; + return data.count; }; export const useGetAccessPolicyApprovalCount = ({ @@ -57,7 +57,7 @@ export const useGetAccessPolicyApprovalCount = ({ const fetchApprovalPolicies = async ({ projectSlug }: TGetAccessApprovalRequestsDTO) => { const { data } = await apiRequest.get<{ approvals: TAccessApprovalPolicy[] }>( - "/api/v1/access-approvals", + "/api/v1/access-approvals/policies", { params: { projectSlug } } ); return data.approvals; @@ -69,7 +69,7 @@ const fetchApprovalRequests = async ({ authorProjectMembershipId }: TGetAccessApprovalRequestsDTO) => { const { data } = await apiRequest.get<{ requests: TAccessApprovalRequest[] }>( - "/api/v1/access-approval-requests", + "/api/v1/access-approvals/requests", { params: { projectSlug, envSlug, authorProjectMembershipId } } ); @@ -90,7 +90,7 @@ const fetchApprovalRequests = async ({ const fetchAccessRequestsCount = async (projectSlug: string) => { const { data } = await apiRequest.get( - "/api/v1/access-approval-requests/count", + "/api/v1/access-approvals/requests/count", { params: { projectSlug } } ); return data; From 5884565de74c28d77611e6a5aa77f117ef41fc2d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:19:17 -0700 Subject: [PATCH 093/158] Fix: Make verifyApprovers independent on memberships --- .../access-approval-policy-fns.ts | 7 +++---- .../access-approval-policy-types.ts | 12 +++--------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts index b570c32fa..7b0a2681f 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts @@ -7,7 +7,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/pr import { TVerifyApprovers } from "./access-approval-policy-types"; export const verifyApprovers = async ({ - approverProjectMemberships, + userIds, projectId, orgId, envSlug, @@ -15,12 +15,11 @@ export const verifyApprovers = async ({ secretPath, permissionService }: TVerifyApprovers) => { - for (const approver of approverProjectMemberships) { + for await (const userId of userIds) { try { - // eslint-disable-next-line no-await-in-loop const { permission: approverPermission } = await permissionService.getProjectPermission( ActorType.USER, - approver.userId, + userId, projectId, actorAuthMethod, orgId diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts index c7b452771..601561b68 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts @@ -1,11 +1,10 @@ -import { TProjectMemberships } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; import { ActorAuthMethod } from "@app/services/auth/auth-type"; import { TPermissionServiceFactory } from "../permission/permission-service"; export type TVerifyApprovers = { - approverProjectMemberships: TProjectMemberships[]; + userIds: string[]; permissionService: Pick; envSlug: string; actorAuthMethod: ActorAuthMethod; @@ -16,7 +15,7 @@ export type TVerifyApprovers = { export type TCreateAccessApprovalPolicy = { approvals: number; - secretPath?: string | null; + secretPath: string; environment: string; approvers: string[]; projectSlug: string; @@ -26,7 +25,7 @@ export type TCreateAccessApprovalPolicy = { export type TUpdateAccessApprovalPolicy = { policyId: string; approvals?: number; - approvers: string[]; + approvers?: string[]; secretPath?: string; name?: string; } & Omit; @@ -43,8 +42,3 @@ export type TGetAccessPolicyCountByEnvironmentDTO = { export type TListAccessApprovalPoliciesDTO = { projectSlug: string; } & Omit; - -export type TGetBoardAccessApprovalPolicy = { - projectId: string; - environment: string; -} & Omit; From 2c1eecaf85acdaa1a6358b7d14604b87d6155891 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:19:43 -0700 Subject: [PATCH 094/158] Chore: Moved verifyApprovers --- .../access-approval-policy-service.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index c384dd71a..51a51abb5 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -54,7 +54,6 @@ export const accessApprovalPolicyServiceFactory = ({ if (approvals > approvers.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); - if (!secretPath) throw new BadRequestError({ message: "Secret path is required" }); const { permission } = await permissionService.getProjectPermission( actor, @@ -75,6 +74,10 @@ export const accessApprovalPolicyServiceFactory = ({ $in: { id: approvers } }); + if (secretApprovers.length !== approvers.length) { + throw new BadRequestError({ message: "Approver not found in project" }); + } + await verifyApprovers({ projectId: project.id, orgId: actorOrgId, @@ -82,13 +85,9 @@ export const accessApprovalPolicyServiceFactory = ({ secretPath, actorAuthMethod, permissionService, - approverProjectMemberships: secretApprovers + userIds: secretApprovers.map((approver) => approver.userId) }); - if (secretApprovers.length !== approvers.length) { - throw new BadRequestError({ message: "Approver not found in project" }); - } - const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => { const doc = await accessApprovalPolicyDAL.create( { @@ -111,7 +110,7 @@ export const accessApprovalPolicyServiceFactory = ({ return { ...accessApproval, environment: env, projectId: project.id }; }; - const getAccessApprovalPolicyByProjectId = async ({ + const getAccessApprovalPolicyByProjectSlug = async ({ actorId, actor, actorOrgId, @@ -185,7 +184,7 @@ export const accessApprovalPolicyServiceFactory = ({ secretPath: doc.secretPath!, actorAuthMethod, permissionService, - approverProjectMemberships: secretApprovers + userIds: secretApprovers.map((approver) => approver.userId) }); if (secretApprovers.length !== approvers.length) @@ -261,7 +260,7 @@ export const accessApprovalPolicyServiceFactory = ({ const policies = await accessApprovalPolicyDAL.find({ envId: environment.id, projectId: project.id }); if (!policies) throw new BadRequestError({ message: "No policies found" }); - return { policyCount: policies.length }; + return { count: policies.length }; }; return { @@ -269,6 +268,6 @@ export const accessApprovalPolicyServiceFactory = ({ createAccessApprovalPolicy, deleteAccessApprovalPolicy, updateAccessApprovalPolicy, - getAccessApprovalPolicyByProjectId + getAccessApprovalPolicyByProjectSlug }; }; From 0c1103e778af82262dcd3515e0b773bf3dfb70b2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:19:58 -0700 Subject: [PATCH 095/158] Fix: Pick --- .../access-approval-request-service.ts | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index d9762d879..f0714ea34 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -16,7 +16,6 @@ import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/acces import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; -import { TProjectUserAdditionalPrivilegeServiceFactory } from "../project-user-additional-privilege/project-user-additional-privilege-service"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types"; import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal"; import { verifyRequestedPermissions } from "./access-approval-request-fns"; @@ -30,18 +29,29 @@ import { } from "./access-approval-request-types"; type TSecretApprovalRequestServiceFactoryDep = { - additionalPrivilegeService: TProjectUserAdditionalPrivilegeServiceFactory; - additionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory; - permissionService: TPermissionServiceFactory; - accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory; + additionalPrivilegeDAL: Pick; + permissionService: Pick; + accessApprovalPolicyApproverDAL: Pick; projectEnvDAL: Pick; projectDAL: Pick; - accessApprovalRequestDAL: TAccessApprovalRequestDALFactory; - accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory; - accessApprovalRequestReviewerDAL: TAccessApprovalRequestReviewerDALFactory; - projectMembershipDAL: TProjectMembershipDALFactory; - smtpService: TSmtpService; - userDAL: TUserDALFactory; + accessApprovalRequestDAL: Pick< + TAccessApprovalRequestDALFactory, + | "create" + | "findRequestsWithPrivilegeByPolicyIds" + | "findById" + | "transaction" + | "updateById" + | "findOne" + | "getCount" + >; + accessApprovalPolicyDAL: Pick; + accessApprovalRequestReviewerDAL: Pick< + TAccessApprovalRequestReviewerDALFactory, + "create" | "find" | "findOne" | "transaction" + >; + projectMembershipDAL: Pick; + smtpService: Pick; + userDAL: Pick; }; export type TAccessApprovalRequestServiceFactory = ReturnType; @@ -107,10 +117,6 @@ export const accessApprovalRequestServiceFactory = ({ approvers.map((approver) => approver.approverId) ); - if (approverUsers.length !== approvers.length) { - throw new BadRequestError({ message: "Some approvers were not found" }); - } - const duplicateRequest = await accessApprovalRequestDAL.findOne({ policyId: policy.id, requestedBy: membership.id, @@ -252,7 +258,7 @@ export const accessApprovalRequestServiceFactory = ({ secretPath: accessApprovalRequest.policy.secretPath!, actorAuthMethod, permissionService, - approverProjectMemberships: [reviewerProjectMembership] + userIds: [reviewerProjectMembership.userId] }); const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id }); From 24a286e8986f34cbdc949fef052607079557e74a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:20:05 -0700 Subject: [PATCH 096/158] Update index.ts --- backend/src/server/routes/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 63c5609be..269a97c49 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -625,7 +625,6 @@ export const registerRoutes = async ( accessApprovalRequestReviewerDAL, additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL, projectMembershipDAL, - additionalPrivilegeService: projectUserAdditionalPrivilegeService, accessApprovalPolicyDAL, accessApprovalRequestDAL, projectEnvDAL, From f038b28c1c1c21836bcf6ca1304e70609d381d26 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:20:18 -0700 Subject: [PATCH 097/158] Fix: Moved Divider to v2 --- frontend/src/components/{basic => v2/Divider}/Divider.tsx | 4 +--- frontend/src/components/v2/Divider/index.tsx | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) rename frontend/src/components/{basic => v2/Divider}/Divider.tsx (78%) create mode 100644 frontend/src/components/v2/Divider/index.tsx diff --git a/frontend/src/components/basic/Divider.tsx b/frontend/src/components/v2/Divider/Divider.tsx similarity index 78% rename from frontend/src/components/basic/Divider.tsx rename to frontend/src/components/v2/Divider/Divider.tsx index f1a570d7b..39b0f84c5 100644 --- a/frontend/src/components/basic/Divider.tsx +++ b/frontend/src/components/v2/Divider/Divider.tsx @@ -4,12 +4,10 @@ interface IProps { className?: string; } -const Divider = ({ className }: IProps): JSX.Element => { +export const Divider = ({ className }: IProps): JSX.Element => { return (
); }; - -export default Divider; diff --git a/frontend/src/components/v2/Divider/index.tsx b/frontend/src/components/v2/Divider/index.tsx new file mode 100644 index 000000000..ac407aa37 --- /dev/null +++ b/frontend/src/components/v2/Divider/index.tsx @@ -0,0 +1 @@ +export { Divider } from "./Divider"; From 6aab28c4c7055730c100867da35ceed95e33a62c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:21:13 -0700 Subject: [PATCH 098/158] Feat: Badge component --- frontend/src/components/v2/Badge/Badge.tsx | 32 ++++++++++++++++++++++ frontend/src/components/v2/Badge/index.tsx | 1 + 2 files changed, 33 insertions(+) create mode 100644 frontend/src/components/v2/Badge/Badge.tsx create mode 100644 frontend/src/components/v2/Badge/index.tsx diff --git a/frontend/src/components/v2/Badge/Badge.tsx b/frontend/src/components/v2/Badge/Badge.tsx new file mode 100644 index 000000000..321c03296 --- /dev/null +++ b/frontend/src/components/v2/Badge/Badge.tsx @@ -0,0 +1,32 @@ +import { cva, VariantProps } from "cva"; +import { twMerge } from "tailwind-merge"; + +interface IProps { + children: React.ReactNode; + className?: string; +} + +const badgeVariants = cva( + [ + "inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-xs text-yellow opacity-80 hover:opacity-100" + ], + { + variants: { + variant: { + primary: "bg-yellow/20 text-yellow", + danger: "bg-red/20 text-red", + success: "bg-green/20 text-green" + } + } + } +); + +export type BadgeProps = VariantProps & IProps; + +export const Badge = ({ children, className, variant }: BadgeProps) => { + return ( +
+ {children} +
+ ); +}; diff --git a/frontend/src/components/v2/Badge/index.tsx b/frontend/src/components/v2/Badge/index.tsx new file mode 100644 index 000000000..5c7042709 --- /dev/null +++ b/frontend/src/components/v2/Badge/index.tsx @@ -0,0 +1 @@ +export { Badge } from "./Badge"; From 3d3b1eb21acb50b1aac29420de4ab866b71cd34c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:21:26 -0700 Subject: [PATCH 099/158] Fix: Use username instead of email --- frontend/src/pages/login/select-organization.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/pages/login/select-organization.tsx b/frontend/src/pages/login/select-organization.tsx index 586ed62f9..de866a323 100644 --- a/frontend/src/pages/login/select-organization.tsx +++ b/frontend/src/pages/login/select-organization.tsx @@ -35,8 +35,6 @@ export default function LoginPage() { const selectOrg = useSelectOrganization(); const { user, isLoading: userLoading } = useUser(); - - const queryParams = new URLSearchParams(window.location.search); const logout = useLogoutUser(true); @@ -153,7 +151,7 @@ export default function LoginPage() {

- You‘re currently logged in as {user.email} + You‘re currently logged in as {user.username}

Not you?{" "} From cdd836d58f156150b303405cc8bb16cc05543b47 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:21:35 -0700 Subject: [PATCH 100/158] Fix: Columns --- .../AccessApprovalPolicyList/AccessApprovalPolicyList.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx index 3e8fdb3b9..aa47cce80 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx @@ -30,7 +30,7 @@ import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/quer import { TAccessApprovalPolicy } from "@app/hooks/api/types"; import { AccessApprovalPolicyRow } from "./components/AccessApprovalPolicyRow"; -import { AccessPolicyForm } from "./components/AccessPolicyForm"; +import { AccessPolicyForm } from "./components/AccessPolicyModal"; interface IProps { workspaceId: string; @@ -127,11 +127,11 @@ export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => { {isPoliciesLoading && ( - + )} {!isPoliciesLoading && !policies?.length && ( - + From 8ed3c0cd68df192750fcbf120045f9bc01538a29 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:21:47 -0700 Subject: [PATCH 101/158] Fix: Use username instead of email --- .../components/AccessApprovalPolicyRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx index 8554e928e..8476bac8d 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx @@ -98,7 +98,7 @@ export const AccessApprovalPolicyRow = ({ iconPos="right" icon={isChecked && } > - {user.email} + {user.username} ); })} From 3637152a6bd628caa432005842afb03bc40a61ed Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:21:57 -0700 Subject: [PATCH 102/158] Chore: Remove unused files --- .../components/AccessPolicyForm.tsx | 266 ------------------ .../components/AccessApprovalPolicyRow.tsx | 146 ---------- .../components/AccessPolicyForm.tsx | 261 ----------------- 3 files changed, 673 deletions(-) delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx deleted file mode 100644 index dd51ea53d..000000000 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ /dev/null @@ -1,266 +0,0 @@ -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 { createNotification } from "@app/components/notifications"; -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, - FormControl, - Input, - Modal, - ModalContent, - Select, - SelectItem -} from "@app/components/v2"; -import { useWorkspace } from "@app/context"; -import { - useCreateAccessApprovalPolicy, - useUpdateAccessApprovalPolicy -} from "@app/hooks/api/accessApproval"; -import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types"; -import { TWorkspaceUser } from "@app/hooks/api/users/types"; - -type Props = { - isOpen?: boolean; - onToggle: (isOpen: boolean) => void; - members?: TWorkspaceUser[]; - projectSlug: string; - editValues?: TAccessApprovalPolicy; -}; - -const formSchema = z - .object({ - environment: z.string(), - name: z.string().optional(), - secretPath: z.string().optional(), - approvals: z.number().min(1), - approvers: z.string().array().min(1) - }) - .refine((data) => data.approvals <= data.approvers.length, { - path: ["approvals"], - message: "The number of approvals should be lower than the number of approvers." - }); - -type TFormSchema = z.infer; - -export const AccessPolicyForm = ({ - isOpen, - onToggle, - members = [], - projectSlug, - editValues -}: Props) => { - const { - control, - handleSubmit, - reset, - formState: { isSubmitting } - } = useForm({ - resolver: zodResolver(formSchema), - values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined - }); - const { currentWorkspace } = useWorkspace(); - - const environments = currentWorkspace?.environments || []; - useEffect(() => { - if (!isOpen) reset({}); - }, [isOpen]); - - const isEditMode = Boolean(editValues); - - const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy(); - const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy(); - - const handleCreatePolicy = async (data: TFormSchema) => { - if (!projectSlug) return; - - try { - await createAccessApprovalPolicy({ - ...data, - projectSlug - }); - 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 (!projectSlug) return; - if (!editValues?.id) return; - - try { - await updateAccessApprovalPolicy({ - id: editValues?.id, - ...data, - projectSlug - }); - 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 are allowed to 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} - - ); - })} - - - - )} - /> - ( - - field.onChange(parseInt(el.target.value, 10))} - /> - - )} - /> -

- - -
- - - - ); -}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx deleted file mode 100644 index 8554e928e..000000000 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { useState } from "react"; -import { faCheckCircle, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { ProjectPermissionCan } from "@app/components/permissions"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, - IconButton, - Input, - Td, - Tr -} from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; -import { useUpdateAccessApprovalPolicy } from "@app/hooks/api"; -import { TAccessApprovalPolicy } from "@app/hooks/api/types"; -import { TWorkspaceUser } from "@app/hooks/api/users/types"; - -type Props = { - policy: TAccessApprovalPolicy; - members?: TWorkspaceUser[]; - projectSlug: string; - onEdit: () => void; - onDelete: () => void; -}; - -export const AccessApprovalPolicyRow = ({ - policy, - members = [], - projectSlug, - onEdit, - onDelete -}: Props) => { - const [selectedApprovers, setSelectedApprovers] = useState([]); - const { mutate: updateAccessApprovalPolicy, isLoading } = useUpdateAccessApprovalPolicy(); - const { permission } = useProjectPermission(); - - return ( - - {policy.name} - {policy.environment.slug} - {policy.secretPath || "*"} - - { - if (!isOpen) { - updateAccessApprovalPolicy( - { - projectSlug, - id: policy.id, - approvers: selectedApprovers - }, - { - onSettled: () => { - setSelectedApprovers([]); - } - } - ); - } else { - setSelectedApprovers(policy.approvers); - } - }} - > - - - - - - Select members that are allowed to 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} - -
- - {(isAllowed) => ( - - - - )} - - - {(isAllowed) => ( - - - - )} - -
- - - ); -}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx deleted file mode 100644 index 7e88da100..000000000 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx +++ /dev/null @@ -1,261 +0,0 @@ -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 { createNotification } from "@app/components/notifications"; -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, - FormControl, - Input, - Modal, - ModalContent, - Select, - SelectItem -} from "@app/components/v2"; -import { useWorkspace } from "@app/context"; -import { - useCreateAccessApprovalPolicy, - useUpdateAccessApprovalPolicy -} from "@app/hooks/api/accessApproval"; -import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types"; -import { TWorkspaceUser } from "@app/hooks/api/users/types"; - -type Props = { - isOpen?: boolean; - onToggle: (isOpen: boolean) => void; - members?: TWorkspaceUser[]; - projectSlug: string; - editValues?: TAccessApprovalPolicy; -}; - -const formSchema = z - .object({ - environment: z.string(), - name: z.string().optional(), - secretPath: z.string().optional(), - approvals: z.number().min(1), - approvers: z.string().array().min(1) - }) - .refine((data) => data.approvals <= data.approvers.length, { - path: ["approvals"], - message: "The number of approvals should be lower than the number of approvers." - }); - -type TFormSchema = z.infer; - -export const AccessPolicyForm = ({ - isOpen, - onToggle, - members = [], - projectSlug, - editValues -}: Props) => { - const { - control, - handleSubmit, - reset, - formState: { isSubmitting } - } = useForm({ - resolver: zodResolver(formSchema), - values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined - }); - const { currentWorkspace } = useWorkspace(); - - const environments = currentWorkspace?.environments || []; - useEffect(() => { - if (!isOpen) reset({}); - }, [isOpen]); - - const isEditMode = Boolean(editValues); - - const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy(); - const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy(); - - const handleCreatePolicy = async (data: TFormSchema) => { - try { - await createAccessApprovalPolicy({ - ...data, - projectSlug - }); - 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 updateAccessApprovalPolicy({ - id: editValues?.id, - ...data, - projectSlug - }); - 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 are allowed to 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} - - ); - })} - - - - )} - /> - ( - - field.onChange(parseInt(el.target.value, 10))} - /> - - )} - /> -
- - -
- -
-
- ); -}; From c325674da0ccea8490202cfb9f31f4eb06cf7cab Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:22:21 -0700 Subject: [PATCH 103/158] Fix: Move standalone components to individual files --- .../components/AccessPolicyModal.tsx | 266 ++++++++++++++++++ .../components/RequestAccessModal.tsx | 25 ++ .../components/ReviewAccessModal.tsx | 158 +++++++++++ 3 files changed, 449 insertions(+) create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/RequestAccessModal.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx new file mode 100644 index 000000000..6c0ee3fb6 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx @@ -0,0 +1,266 @@ +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 { createNotification } from "@app/components/notifications"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { + useCreateAccessApprovalPolicy, + useUpdateAccessApprovalPolicy +} from "@app/hooks/api/accessApproval"; +import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types"; +import { TWorkspaceUser } from "@app/hooks/api/users/types"; + +type Props = { + isOpen?: boolean; + onToggle: (isOpen: boolean) => void; + members?: TWorkspaceUser[]; + projectSlug: string; + editValues?: TAccessApprovalPolicy; +}; + +const formSchema = z + .object({ + environment: z.string(), + name: z.string().optional(), + secretPath: z.string().optional(), + approvals: z.number().min(1), + approvers: z.string().array().min(1) + }) + .refine((data) => data.approvals <= data.approvers.length, { + path: ["approvals"], + message: "The number of approvals should be lower than the number of approvers." + }); + +type TFormSchema = z.infer; + +export const AccessPolicyForm = ({ + isOpen, + onToggle, + members = [], + projectSlug, + editValues +}: Props) => { + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema), + values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined + }); + const { currentWorkspace } = useWorkspace(); + + const environments = currentWorkspace?.environments || []; + useEffect(() => { + if (!isOpen) reset({}); + }, [isOpen]); + + const isEditMode = Boolean(editValues); + + const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy(); + const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy(); + + const handleCreatePolicy = async (data: TFormSchema) => { + if (!projectSlug) return; + + try { + await createAccessApprovalPolicy({ + ...data, + projectSlug + }); + 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 (!projectSlug) return; + if (!editValues?.id) return; + + try { + await updateAccessApprovalPolicy({ + id: editValues?.id, + ...data, + projectSlug + }); + 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 are allowed to 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.username} + + ); + })} + + + + )} + /> + ( + + field.onChange(parseInt(el.target.value, 10))} + /> + + )} + /> +
+ + +
+ +
+
+ ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/RequestAccessModal.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/RequestAccessModal.tsx new file mode 100644 index 000000000..735ca3b53 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/RequestAccessModal.tsx @@ -0,0 +1,25 @@ +import { Modal, ModalContent } from "@app/components/v2"; +import { TAccessApprovalPolicy } from "@app/hooks/api/types"; +import { SpecificPrivilegeSecretForm } from "@app/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection"; + +export const RequestAccessModal = ({ + isOpen, + onOpenChange, + policies +}: { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + policies: TAccessApprovalPolicy[]; +}) => { + return ( + + + onOpenChange(false)} policies={policies} /> + + + ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx new file mode 100644 index 000000000..481cee9c6 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx @@ -0,0 +1,158 @@ +import { useCallback, useMemo, useState } from "react"; +import ms from "ms"; + +import { createNotification } from "@app/components/notifications"; +import { Button, Modal, ModalContent } from "@app/components/v2"; +import { Badge } from "@app/components/v2/Badge"; +import { ProjectPermissionActions } from "@app/context"; +import { useReviewAccessRequest } from "@app/hooks/api"; +import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types"; +import { TWorkspaceUser } from "@app/hooks/api/types"; + +export const ReviewAccessRequestModal = ({ + isOpen, + onOpenChange, + request, + projectSlug, + selectedRequester, + selectedEnvSlug +}: { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + request: TAccessApprovalRequest & { user: TWorkspaceUser["user"] | null }; + projectSlug: string; + selectedRequester: string | undefined; + selectedEnvSlug: string | undefined; +}) => { + const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null); + + const accessDetails = { + env: request.environmentName, + // secret path will be inside $glob operator + secretPath: request.policy.secretPath, + read: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Read)), + edit: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Edit)), + create: request.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Create) + ), + delete: request.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Delete) + ), + + temporaryAccess: { + isTemporary: request.isTemporary, + temporaryRange: request.temporaryRange + } + }; + + const requestedAccess = useMemo(() => { + const access: string[] = []; + if (accessDetails.read) access.push("Read"); + if (accessDetails.edit) access.push("Edit"); + if (accessDetails.create) access.push("Create"); + if (accessDetails.delete) access.push("Delete"); + + return access.join(", "); + }, [accessDetails]); + + const getAccessLabel = () => { + if (!accessDetails.temporaryAccess.isTemporary || !accessDetails.temporaryAccess.temporaryRange) + return "Permanent"; + + // convert the range to human readable format + ms(ms(accessDetails.temporaryAccess.temporaryRange), { long: true }); + + return ( + + {`Valid for ${ms(ms(accessDetails.temporaryAccess.temporaryRange), { + long: true + })} after approval`} + + ); + }; + + const reviewAccessRequest = useReviewAccessRequest(); + + const handleReview = useCallback(async (status: "approved" | "rejected") => { + setIsLoading(status); + try { + await reviewAccessRequest.mutateAsync({ + requestId: request.id, + status, + projectSlug, + envSlug: selectedEnvSlug, + requestedBy: selectedRequester + }); + } catch (error) { + console.error(error); + setIsLoading(null); + return; + } + + createNotification({ + title: `Request ${status}`, + text: `The request has been ${status}`, + type: status === "approved" ? "success" : "info" + }); + + setIsLoading(null); + onOpenChange(false); + }, []); + + return ( + + +
+ + + {request.user?.firstName} {request.user?.lastName} ({request.user?.email}) + {" "} + is requesting access to the following resource: + + +
+
+ Requested path: + {accessDetails.env + accessDetails.secretPath || ""} +
+ +
+ Permissions: + {requestedAccess} +
+ +
+ Access Type: + {getAccessLabel()} +
+
+ +
+ + +
+
+
+
+ ); +}; From a3d7c5f5996121d856e0b615c850c343aa38c247 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:22:33 -0700 Subject: [PATCH 104/158] Cleanup --- .../AccessApprovalRequest.tsx | 229 ++---------------- 1 file changed, 22 insertions(+), 207 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index b6d69d0c9..235737d4a 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -1,6 +1,6 @@ /* eslint-disable no-nested-ternary */ /* eslint-disable react/jsx-no-useless-fragment */ -import { useCallback, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { faCheck, faCheckCircle, @@ -11,10 +11,7 @@ import { import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { formatDistance } from "date-fns"; import { AnimatePresence, motion } from "framer-motion"; -import ms from "ms"; -import { twMerge } from "tailwind-merge"; -import { createNotification } from "@app/components/notifications"; import { Button, DropdownMenu, @@ -23,11 +20,10 @@ import { DropdownMenuLabel, DropdownMenuTrigger, EmptyState, - Modal, - ModalContent, Tooltip, UpgradePlanModal } from "@app/components/v2"; +import { Badge } from "@app/components/v2/Badge"; import { ProjectPermissionActions, ProjectPermissionSub, @@ -36,7 +32,7 @@ import { useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; -import { useGetWorkspaceUsers, useReviewAccessRequest } from "@app/hooks/api"; +import { useGetWorkspaceUsers } from "@app/hooks/api"; import { accessApprovalKeys, useGetAccessApprovalPolicies, @@ -44,192 +40,11 @@ import { useGetAccessRequestsCount } from "@app/hooks/api/accessApproval/queries"; import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types"; -import { ApprovalStatus, TAccessApprovalPolicy, TWorkspaceUser } from "@app/hooks/api/types"; +import { ApprovalStatus, TWorkspaceUser } from "@app/hooks/api/types"; import { queryClient } from "@app/reactQuery"; -import { SpecificPrivilegeSecretForm } from "@app/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection"; -const DisplayBadge = ({ text, className }: { text: string; className?: string }) => { - return ( -
- {text} -
- ); -}; - -const ReviewRequestModal = ({ - isOpen, - onOpenChange, - request, - projectSlug, - selectedRequester, - selectedEnvSlug -}: { - isOpen: boolean; - onOpenChange: (isOpen: boolean) => void; - request: TAccessApprovalRequest & { user: TWorkspaceUser["user"] | null }; - projectSlug: string; - selectedRequester: string | undefined; - selectedEnvSlug: string | undefined; -}) => { - const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null); - - const accessDetails = { - env: request.environmentName, - // secret path will be inside $glob operator - secretPath: request.policy.secretPath, - read: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Read)), - edit: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Edit)), - create: request.permissions?.some(({ action }) => - action.includes(ProjectPermissionActions.Create) - ), - delete: request.permissions?.some(({ action }) => - action.includes(ProjectPermissionActions.Delete) - ), - - temporaryAccess: { - isTemporary: request.isTemporary, - temporaryRange: request.temporaryRange - } - }; - - const requestedAccess = useMemo(() => { - const access: string[] = []; - if (accessDetails.read) access.push("Read"); - if (accessDetails.edit) access.push("Edit"); - if (accessDetails.create) access.push("Create"); - if (accessDetails.delete) access.push("Delete"); - - return access.join(", "); - }, [accessDetails]); - - const getAccessLabel = () => { - if (!accessDetails.temporaryAccess.isTemporary || !accessDetails.temporaryAccess.temporaryRange) - return "Permanent"; - - // convert the range to human readable format - ms(ms(accessDetails.temporaryAccess.temporaryRange), { long: true }); - - return ( - - ); - }; - - const reviewAccessRequest = useReviewAccessRequest(); - - const handleReview = useCallback(async (status: "approved" | "rejected") => { - setIsLoading(status); - try { - await reviewAccessRequest.mutateAsync({ - requestId: request.id, - status, - projectSlug, - envSlug: selectedEnvSlug, - requestedBy: selectedRequester - }); - } catch (error) { - console.error(error); - setIsLoading(null); - return; - } - - createNotification({ - title: `Request ${status}`, - text: `The request has been ${status}`, - type: status === "approved" ? "success" : "info" - }); - - setIsLoading(null); - onOpenChange(false); - }, []); - - return ( - - -
- - - {request.user?.firstName} {request.user?.lastName} ({request.user?.email}) - {" "} - is requesting access to the following resource: - - -
-
- Requested path: - -
- -
- Permissions: - -
- -
- Access Type: - {getAccessLabel()} -
-
- -
- - -
-
-
-
- ); -}; - -const SelectAccessModal = ({ - isOpen, - onOpenChange, - policies -}: { - isOpen: boolean; - onOpenChange: (isOpen: boolean) => void; - policies: TAccessApprovalPolicy[]; -}) => { - return ( - - - onOpenChange(false)} policies={policies} /> - - - ); -}; +import { RequestAccessModal } from "./components/RequestAccessModal"; +import { ReviewAccessRequestModal } from "./components/ReviewAccessModal"; const generateRequestText = (request: TAccessApprovalRequest, membershipId: string) => { const { isTemporary } = request; @@ -249,7 +64,7 @@ const generateRequestText = (request: TAccessApprovalRequest, membershipId: stri
{request.requestedBy === membershipId && ( - + Requested By You )}
@@ -331,29 +146,30 @@ export const AccessApprovalRequest = ({ ({ member }) => member === membership.id )?.status; - let displayData: { label: string; colorClass: string } = { label: "", colorClass: "" }; + let displayData: { label: string; type: "primary" | "danger" | "success" } = { + label: "", + type: "primary" + }; const isExpired = request.privilege && request.isApproved && new Date() > new Date(request.privilege.temporaryAccessEndTime || ("" as string)); - if (isExpired) displayData = { label: "Access Expired", colorClass: "bg-red/20 text-red" }; - else if (isAccepted) - displayData = { label: "Access Granted", colorClass: "bg-green/20 text-green" }; - else if (isRejectedByAnyone) - displayData = { label: "Rejected", colorClass: "bg-red/20 text-red" }; + if (isExpired) displayData = { label: "Access Expired", type: "danger" }; + else if (isAccepted) displayData = { label: "Access Granted", type: "success" }; + else if (isRejectedByAnyone) displayData = { label: "Rejected", type: "danger" }; else if (userReviewStatus === ApprovalStatus.APPROVED) { displayData = { label: `Pending ${request.policy.approvals - request.reviewers.length} review${ request.policy.approvals - request.reviewers.length > 1 ? "s" : "" }`, - colorClass: "bg-yellow/20 text-yellow" + type: "primary" }; } else if (!isReviewedByUser) displayData = { label: "Review Required", - colorClass: "bg-yellow/20 text-yellow" + type: "primary" }; return { @@ -486,7 +302,7 @@ export const AccessApprovalRequest = ({ icon={requestedByFilter === id && } iconPos="right" > - {user.email} + {user.username} ))} @@ -564,10 +380,9 @@ export const AccessApprovalRequest = ({
{details.isApprover && ( - + + {details.displayData.label} + )}
@@ -581,7 +396,7 @@ export const AccessApprovalRequest = ({ {!!policies && ( - { @@ -598,7 +413,7 @@ export const AccessApprovalRequest = ({ )} {!!selectedRequest && ( - Date: Tue, 9 Apr 2024 00:22:47 -0700 Subject: [PATCH 105/158] Fix: Moved from email to username --- .../components/SecretApprovalPolicyRow.tsx | 2 +- .../SecretApprovalPolicyList/components/SecretPolicyForm.tsx | 2 +- .../components/SecretApprovalRequest/SecretApprovalRequest.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx index c256af1c7..e0e4dd5fc 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx @@ -98,7 +98,7 @@ export const SecretApprovalPolicyRow = ({ iconPos="right" icon={isChecked && } > - {user.email} + {user.username} ); })} diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx index 185c596c2..db59761c1 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx @@ -222,7 +222,7 @@ export const SecretPolicyForm = ({ iconPos="right" icon={isChecked && } > - {user.email} + {user.username} ); })} diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx index 32a7ff6de..0d0c6213a 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx @@ -188,7 +188,7 @@ export const SecretApprovalRequest = () => { icon={committerFilter === id && } iconPos="right" > - {user.email} + {user.username} ))} From 0bbdf2a8f48db323358d8773f8ba3759186e32c2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:22:52 -0700 Subject: [PATCH 106/158] Update SecretApprovalPage.tsx --- frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index d66a6c8cc..3878ecdca 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -2,8 +2,8 @@ import Link from "next/link"; import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import Divider from "@app/components/basic/Divider"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { Divider } from "@app/components/v2/Divider"; import { useWorkspace } from "@app/context"; import { AccessApprovalPolicyList } from "./components/AccessApprovalPolicyList"; From d659b5a6249bf34594d4312ac52ab79b8342e5e2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 12 Apr 2024 03:58:50 +0200 Subject: [PATCH 107/158] Fix: Duplicate access request check --- .../access-approval-request-service.ts | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index f0714ea34..becdb78da 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -37,6 +37,7 @@ type TSecretApprovalRequestServiceFactoryDep = { accessApprovalRequestDAL: Pick< TAccessApprovalRequestDALFactory, | "create" + | "find" | "findRequestsWithPrivilegeByPolicyIds" | "findById" | "transaction" @@ -117,31 +118,33 @@ export const accessApprovalRequestServiceFactory = ({ approvers.map((approver) => approver.approverId) ); - const duplicateRequest = await accessApprovalRequestDAL.findOne({ + const duplicateRequests = await accessApprovalRequestDAL.find({ policyId: policy.id, requestedBy: membership.id, permissions: JSON.stringify(requestedPermissions), isTemporary }); - if (duplicateRequest) { - if (duplicateRequest.privilegeId) { - const privilege = await additionalPrivilegeDAL.findById(duplicateRequest.privilegeId); + if (duplicateRequests?.length > 0) { + for await (const duplicateRequest of duplicateRequests) { + if (duplicateRequest.privilegeId) { + const privilege = await additionalPrivilegeDAL.findById(duplicateRequest.privilegeId); - const isExpired = new Date() > new Date(privilege.temporaryAccessEndTime || ("" as string)); + const isExpired = new Date() > new Date(privilege.temporaryAccessEndTime || ("" as string)); - if (!isExpired || !privilege.isTemporary) { - throw new BadRequestError({ message: "You already have an active privilege with the same criteria" }); - } - } else { - const reviewers = await accessApprovalRequestReviewerDAL.find({ - requestId: duplicateRequest.id - }); + if (!isExpired || !privilege.isTemporary) { + throw new BadRequestError({ message: "You already have an active privilege with the same criteria" }); + } + } else { + const reviewers = await accessApprovalRequestReviewerDAL.find({ + requestId: duplicateRequest.id + }); - const isRejected = reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED); + const isRejected = reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED); - if (!isRejected) { - throw new BadRequestError({ message: "You already have a pending access request with the same criteria" }); + if (!isRejected) { + throw new BadRequestError({ message: "You already have a pending access request with the same criteria" }); + } } } } From 7ac3bb20df65beddee029ae0e54c0aeb6cfa58e8 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 12 Apr 2024 11:27:20 -0700 Subject: [PATCH 108/158] Update instance recognition of offline license --- backend/src/ee/services/license/license-service.ts | 4 ++-- backend/src/ee/services/license/license-types.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index e81f6dc12..47b46d010 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -121,8 +121,8 @@ export const licenseServiceFactory = ({ if (isValidOfflineLicense) { onPremFeatures = contents.license.features; - instanceType = InstanceType.EnterpriseOnPrem; - logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`); + instanceType = InstanceType.EnterpriseOnPremOffline; + logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`); isValidLicense = true; return; } diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index a2379ddaa..0c8fdc197 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -3,6 +3,7 @@ import { TOrgPermission } from "@app/lib/types"; export enum InstanceType { OnPrem = "self-hosted", EnterpriseOnPrem = "enterprise-self-hosted", + EnterpriseOnPremOffline = "enterprise-self-hosted-offline", Cloud = "cloud" } From 73d9fcc0de3b632cf0b22190f41e26d3d6bdd927 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 29 Apr 2024 19:11:13 +0200 Subject: [PATCH 109/158] Draft --- .../MemberRoleForm/SpecificPrivilegeSection.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx index 795ccbb24..d39a88aa2 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -369,13 +369,13 @@ export const SpecificPrivilegeSecretForm = ({ } return ( - - + + ); }} /> From 2aacd5411636a9040dfd653d0366eed60b228768 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 29 Apr 2024 19:11:36 +0200 Subject: [PATCH 110/158] Update SpecificPrivilegeSection.tsx --- .../MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx index d39a88aa2..6fb97063b 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -134,7 +134,6 @@ export const SpecificPrivilegeSecretForm = ({ }); const temporaryAccessField = privilegeForm.watch("temporaryAccess"); - const selectedEnvironmentSlug = privilegeForm.watch("environmentSlug"); const selectedEnvironment = privilegeForm.watch("environmentSlug"); const secretPath = privilegeForm.watch("secretPath"); @@ -373,7 +372,7 @@ export const SpecificPrivilegeSecretForm = ({ {...field} isDisabled={isMemberEditDisabled} containerClassName="w-48" - environment={selectedEnvironmentSlug} + environment={selectedEnvironment} /> ); From 5894df4370556728099e394780d8595295dbd0e4 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 29 Mar 2024 16:24:15 +0100 Subject: [PATCH 111/158] Draft --- backend/src/ee/services/license/licence-fns.ts | 2 +- frontend/src/components/basic/Divider.tsx | 15 +++++++++++++++ .../permissions/PermissionDeniedBanner.tsx | 5 +++++ .../SecretApprovalPage/SecretApprovalPage.tsx | 6 ++++++ 4 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/basic/Divider.tsx diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 189a3c4e0..492d09ec3 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ status: null, trial_end: null, has_used_trial: true, - secretApproval: false, + secretApproval: true, secretRotation: true }); diff --git a/frontend/src/components/basic/Divider.tsx b/frontend/src/components/basic/Divider.tsx new file mode 100644 index 000000000..f1a570d7b --- /dev/null +++ b/frontend/src/components/basic/Divider.tsx @@ -0,0 +1,15 @@ +import { twMerge } from "tailwind-merge"; + +interface IProps { + className?: string; +} + +const Divider = ({ className }: IProps): JSX.Element => { + return ( +
+ + ); +}; + +export default Divider; diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx index b3c7a4f53..f9707d634 100644 --- a/frontend/src/components/permissions/PermissionDeniedBanner.tsx +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -3,6 +3,8 @@ import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; +import { Button } from "../v2"; + type Props = { containerClassName?: string; className?: string; @@ -32,6 +34,9 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children )}
+
); diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 3878ecdca..fa083b908 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -56,13 +56,19 @@ export const SecretApprovalPage = () => { + + + + + + From 810f670e64fea1f6096320a6166d3c0a99fc3b4b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 1 Apr 2024 08:22:46 -0700 Subject: [PATCH 112/158] Feat: Request Access --- backend/src/ee/routes/v1/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index fc5c0865d..16e23eb88 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,6 +1,6 @@ -import { registerAuditLogStreamRouter } from "./audit-log-stream-router"; import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router"; import { registerAccessApprovalRequestRouter } from "./access-approval-request-router"; +import { registerAuditLogStreamRouter } from "./audit-log-stream-router"; import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerGroupRouter } from "./group-router"; From 3331699f56be77e8bfd85d167c27db1e1e959acf Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 1 Apr 2024 09:49:22 -0700 Subject: [PATCH 113/158] Feat: Request access --- backend/scripts/generate-schema-types.ts | 2 +- .../20240330075122_access-approval-policy.ts | 3 +- .../access-approval-policy-service.ts | 1 - .../SecretApprovalPage/SecretApprovalPage.tsx | 7 - .../components/AccessPolicyForm.tsx | 250 ++++++++++++++++++ 5 files changed, 253 insertions(+), 10 deletions(-) create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 8c913991f..28b736152 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -5,7 +5,7 @@ import knex from "knex"; import { writeFileSync } from "fs"; dotenv.config({ - path: path.join(__dirname, "../../.env.migration") + path: path.join(__dirname, "../../.env") }); const db = knex({ diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index 61cb9274c..7aa9da6a0 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -9,8 +9,9 @@ export async function up(knex: Knex): Promise { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.string("name").notNullable(); t.integer("approvals").defaultTo(1).notNullable(); - t.uuid("envId").notNullable(); t.string("secretPath"); + + t.uuid("envId").notNullable(); t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); t.timestamps(true, true, true); }); diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index 51a51abb5..f5cce5db4 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -154,7 +154,6 @@ export const accessApprovalPolicyServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index fa083b908..9c273848f 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -53,22 +53,15 @@ export const SecretApprovalPage = () => { Access Requests Access Request Policies - - - - - - - diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx new file mode 100644 index 000000000..56a4bdef5 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -0,0 +1,250 @@ +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 { createNotification } from "@app/components/notifications"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { + useCreateAccessApprovalPolicy, + useUpdateAccessApprovalPolicy +} from "@app/hooks/api/accessApproval"; +import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types"; +import { TWorkspaceUser } from "@app/hooks/api/users/types"; + +type Props = { + isOpen?: boolean; + onToggle: (isOpen: boolean) => void; + members?: TWorkspaceUser[]; + workspaceId: string; + editValues?: TAccessApprovalPolicy; +}; + +const formSchema = z + .object({ + environment: z.string(), + name: z.string().optional(), + secretPath: z.string().optional().nullable(), + approvals: z.number().min(1), + approvers: z.string().array().min(1) + }) + .refine((data) => data.approvals <= data.approvers.length, { + path: ["approvals"], + message: "The number of approvals should be lower than the number of approvers." + }); + +type TFormSchema = z.infer; + +export const AccessPolicyForm = ({ + isOpen, + onToggle, + members = [], + workspaceId, + editValues +}: Props) => { + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema), + values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined + }); + const { currentWorkspace } = useWorkspace(); + + const environments = currentWorkspace?.environments || []; + useEffect(() => { + if (!isOpen) reset({}); + }, [isOpen]); + + const isEditMode = Boolean(editValues); + + const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy(); + const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy(); + + const handleCreatePolicy = async (data: TFormSchema) => { + try { + await createAccessApprovalPolicy({ + ...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 updateAccessApprovalPolicy({ + id: editValues?.id, + ...data, + 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 are allowed to 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} + + ); + })} + + + + )} + /> + ( + + field.onChange(parseInt(el.target.value, 10))} + /> + + )} + /> +
+ + +
+ +
+
+ ); +}; From 448f89fd1c4d536dfc4a15c53bebb469393ee067 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:35:51 -0700 Subject: [PATCH 114/158] Feat: Request Access (migrations) --- .../src/db/migrations/20240330075122_access-approval-policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index 7aa9da6a0..e6d47d864 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -12,6 +12,7 @@ export async function up(knex: Knex): Promise { t.string("secretPath"); t.uuid("envId").notNullable(); + t.string("secretPath"); t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); t.timestamps(true, true, true); }); @@ -34,7 +35,6 @@ export async function up(knex: Knex): Promise { export async function down(knex: Knex): Promise { await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover); await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); - await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); } From 5c69bbf515f8c8debba601f6b6c8bb33328d1ea4 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:37:49 -0700 Subject: [PATCH 115/158] Feat: Request access (new routes) --- .../v1/access-approval-request-router.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 4b173cfa7..696c8b76f 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -1,7 +1,9 @@ +import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas"; import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -11,12 +13,24 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv method: "POST", schema: { body: z.object({ + slug: z + .string() + .min(1) + .max(60) + .trim() + .default(`requested-privilege-${slugify(alphaNumericNanoId(12))}`) + .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }), permissions: z.any().array(), isTemporary: z.boolean(), temporaryRange: z.string().optional() }), querystring: z.object({ - projectSlug: z.string().trim() + projectSlug: z.string().trim(), + secretPath: z.string().trim(), + envSlug: z.string().trim() }), response: { 200: z.object({ @@ -31,7 +45,9 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, permissions: req.body.permissions, + envSlug: req.query.envSlug, actorOrgId: req.permission.orgId, + secretPath: req.query.secretPath, projectSlug: req.query.projectSlug, temporaryRange: req.body.temporaryRange, isTemporary: req.body.isTemporary From cd6be68461efddd5ce084f11e55de9d57f6bcba4 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:39:15 -0700 Subject: [PATCH 116/158] Fix: Validate approvers access --- .../access-approval-policy/access-approval-policy-service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index f5cce5db4..f14380869 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -54,6 +54,7 @@ export const accessApprovalPolicyServiceFactory = ({ if (approvals > approvers.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); + if (!secretPath) throw new BadRequestError({ message: "Secret path is required" }); const { permission } = await permissionService.getProjectPermission( actor, @@ -154,6 +155,7 @@ export const accessApprovalPolicyServiceFactory = ({ actorAuthMethod, actorOrgId ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { From 812cced9d53e154a3a3cab9827386e2dd1dc6748 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:40:05 -0700 Subject: [PATCH 117/158] Feat: Request access --- .../access-approval-request-secret-dal.ts | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts new file mode 100644 index 000000000..d458d58ff --- /dev/null +++ b/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts @@ -0,0 +1,230 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { + SecretApprovalRequestsSecretsSchema, + TableName, + TSecretApprovalRequestsSecrets, + TSecretTags +} from "@app/db/schemas"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; + +export type TAccessApprovalRequestSecretDALFactory = ReturnType; + +export const accessApprovalRequestSecretDALFactory = (db: TDbClient) => { + const accessApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret); + const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag); + + const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => { + try { + const existingApprovalSecrets = await accessApprovalRequestSecretOrm.find( + { + $in: { + id: data.map((el) => el.id) + } + }, + { tx } + ); + + if (existingApprovalSecrets.length !== data.length) { + throw new BadRequestError({ message: "Some of the secret approvals do not exist" }); + } + + if (data.length === 0) return []; + + const updatedApprovalSecrets = await (tx || db)(TableName.SecretApprovalRequestSecret) + .insert(data) + .onConflict("id") // this will cause a conflict then merge the data + .merge() // Merge the data with the existing data + .returning("*"); + + return updatedApprovalSecrets; + } catch (error) { + throw new DatabaseError({ error, name: "bulk update secret" }); + } + }; + + const findByRequestId = async (requestId: string, tx?: Knex) => { + try { + const doc = await (tx || db)({ + secVerTag: TableName.SecretTag + }) + .from(TableName.SecretApprovalRequestSecret) + .where({ requestId }) + .leftJoin( + TableName.SecretApprovalRequestSecretTag, + `${TableName.SecretApprovalRequestSecret}.id`, + `${TableName.SecretApprovalRequestSecretTag}.secretId` + ) + .leftJoin(TableName.SecretTag, `${TableName.SecretApprovalRequestSecretTag}.tagId`, `${TableName.SecretTag}.id`) + .leftJoin(TableName.Secret, `${TableName.SecretApprovalRequestSecret}.secretId`, `${TableName.Secret}.id`) + .leftJoin( + TableName.SecretVersion, + `${TableName.SecretVersion}.id`, + `${TableName.SecretApprovalRequestSecret}.secretVersion` + ) + .leftJoin( + TableName.SecretVersionTag, + `${TableName.SecretVersionTag}.${TableName.SecretVersion}Id`, + `${TableName.SecretVersion}.id` + ) + .leftJoin( + db.ref(TableName.SecretTag).as("secVerTag"), + `${TableName.SecretVersionTag}.${TableName.SecretTag}Id`, + db.ref("id").withSchema("secVerTag") + ) + .select(selectAllTableCols(TableName.SecretApprovalRequestSecret)) + .select({ + secVerTagId: "secVerTag.id", + secVerTagColor: "secVerTag.color", + secVerTagSlug: "secVerTag.slug", + secVerTagName: "secVerTag.name" + }) + .select( + db.ref("id").withSchema(TableName.SecretTag).as("tagId"), + db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"), + db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), + db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), + db.ref("name").withSchema(TableName.SecretTag).as("tagName") + ) + .select( + db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"), + db.ref("version").withSchema(TableName.Secret).as("orgSecVersion"), + db.ref("secretKeyIV").withSchema(TableName.Secret).as("orgSecKeyIV"), + db.ref("secretKeyTag").withSchema(TableName.Secret).as("orgSecKeyTag"), + db.ref("secretKeyCiphertext").withSchema(TableName.Secret).as("orgSecKeyCiphertext"), + db.ref("secretValueIV").withSchema(TableName.Secret).as("orgSecValueIV"), + db.ref("secretValueTag").withSchema(TableName.Secret).as("orgSecValueTag"), + db.ref("secretValueCiphertext").withSchema(TableName.Secret).as("orgSecValueCiphertext"), + db.ref("secretCommentIV").withSchema(TableName.Secret).as("orgSecCommentIV"), + db.ref("secretCommentTag").withSchema(TableName.Secret).as("orgSecCommentTag"), + db.ref("secretCommentCiphertext").withSchema(TableName.Secret).as("orgSecCommentCiphertext") + ) + .select( + db.ref("version").withSchema(TableName.SecretVersion).as("secVerVersion"), + db.ref("secretKeyIV").withSchema(TableName.SecretVersion).as("secVerKeyIV"), + db.ref("secretKeyTag").withSchema(TableName.SecretVersion).as("secVerKeyTag"), + db.ref("secretKeyCiphertext").withSchema(TableName.SecretVersion).as("secVerKeyCiphertext"), + db.ref("secretValueIV").withSchema(TableName.SecretVersion).as("secVerValueIV"), + db.ref("secretValueTag").withSchema(TableName.SecretVersion).as("secVerValueTag"), + db.ref("secretValueCiphertext").withSchema(TableName.SecretVersion).as("secVerValueCiphertext"), + db.ref("secretCommentIV").withSchema(TableName.SecretVersion).as("secVerCommentIV"), + db.ref("secretCommentTag").withSchema(TableName.SecretVersion).as("secVerCommentTag"), + db.ref("secretCommentCiphertext").withSchema(TableName.SecretVersion).as("secVerCommentCiphertext") + ); + const formatedDoc = sqlNestRelationships({ + data: doc, + key: "id", + parentMapper: (data) => SecretApprovalRequestsSecretsSchema.omit({ secretVersion: true }).parse(data), + childrenMapper: [ + { + key: "tagJnId", + label: "tags" as const, + mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({ + id, + name, + slug, + color + }) + }, + { + key: "secretId", + label: "secret" as const, + mapper: ({ + orgSecKeyIV, + orgSecKeyTag, + orgSecValueIV, + orgSecVersion, + orgSecValueTag, + orgSecCommentIV, + orgSecBlindIndex, + orgSecCommentTag, + orgSecKeyCiphertext, + orgSecValueCiphertext, + orgSecCommentCiphertext, + secretId + }) => + secretId + ? { + id: secretId, + version: orgSecVersion, + secretBlindIndex: orgSecBlindIndex, + secretKeyIV: orgSecKeyIV, + secretKeyTag: orgSecKeyTag, + secretKeyCiphertext: orgSecKeyCiphertext, + secretValueIV: orgSecValueIV, + secretValueTag: orgSecValueTag, + secretValueCiphertext: orgSecValueCiphertext, + secretCommentIV: orgSecCommentIV, + secretCommentTag: orgSecCommentTag, + secretCommentCiphertext: orgSecCommentCiphertext + } + : undefined + }, + { + key: "secretVersion", + label: "secretVersion" as const, + mapper: ({ + secVerCommentIV, + secVerCommentCiphertext, + secVerCommentTag, + secVerValueCiphertext, + secVerKeyIV, + secVerKeyTag, + secVerValueIV, + secretVersion, + secVerValueTag, + secVerKeyCiphertext, + secVerVersion + }) => + secretVersion + ? { + version: secVerVersion, + id: secretVersion, + secretKeyIV: secVerKeyIV, + secretKeyTag: secVerKeyTag, + secretKeyCiphertext: secVerKeyCiphertext, + secretValueIV: secVerValueIV, + secretValueTag: secVerValueTag, + secretValueCiphertext: secVerValueCiphertext, + secretCommentIV: secVerCommentIV, + secretCommentTag: secVerCommentTag, + secretCommentCiphertext: secVerCommentCiphertext + } + : undefined, + childrenMapper: [ + { + key: "secVerTagId", + label: "tags" as const, + mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({ + // eslint-disable-next-line + id, + // eslint-disable-next-line + name, + // eslint-disable-next-line + slug, + // eslint-disable-next-line + color + }) + } + ] + } + ] + }); + return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({ + ...el, + secret: secret?.[0], + secretVersion: secretVersion?.[0] + })); + } catch (error) { + throw new DatabaseError({ error, name: "FindByRequestId" }); + } + }; + return { + ...accessApprovalRequestSecretOrm, + findByRequestId, + bulkUpdateNoVersionIncrement, + insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany + }; +}; From 1ccd74e1a5104064f9ef6195208cec0de55665a2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:43:48 -0700 Subject: [PATCH 118/158] Fix: Remove redundant code --- .../src/components/permissions/PermissionDeniedBanner.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx index f9707d634..b3c7a4f53 100644 --- a/frontend/src/components/permissions/PermissionDeniedBanner.tsx +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -3,8 +3,6 @@ import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { Button } from "../v2"; - type Props = { containerClassName?: string; className?: string; @@ -34,9 +32,6 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children )}
-
); From 4eabbb3ac53ee612b693fd198ee6926b4404d160 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:49:00 -0700 Subject: [PATCH 119/158] Fix: Added support for request access --- .../SpecificPrivilegeSection.tsx | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx index 6fb97063b..dd09226eb 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -311,6 +311,91 @@ export const SpecificPrivilegeSecretForm = ({ )}`; } return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); + }; + + }; + + // This is used for requesting access additional privileges, not directly creating a privilege! + const handleRequestAccess = async (data: TSecretPermissionForm) => { + if (!policies) return; + if (!currentWorkspace) { + createNotification({ + type: "error", + text: "No workspace found.", + title: "Error" + }); + return; + } + + if (!data.secretPath) { + createNotification({ + type: "error", + text: "Please select a secret path", + title: "Error" + }); + return; + } + + const actions = [ + { action: ProjectPermissionActions.Read, allowed: data.read }, + { action: ProjectPermissionActions.Create, allowed: data.create }, + { action: ProjectPermissionActions.Delete, allowed: data.delete }, + { action: ProjectPermissionActions.Edit, allowed: data.edit } + ]; + const conditions: Record = { environment: data.environmentSlug }; + if (data.secretPath) { + conditions.secretPath = { $glob: data.secretPath }; + } + await requestAccess.mutateAsync({ + ...data, + ...(data.temporaryAccess.isTemporary && { + temporaryAccessStartTime: data.temporaryAccess.temporaryAccessStartTime, + temporaryAccessEndTime: data.temporaryAccess.temporaryAccessEndTime, + temporaryRange: data.temporaryAccess.temporaryRange, + temporaryMode: "relative" + }), + envSlug: data.environmentSlug, + secretPath: data.secretPath, + projectSlug: currentWorkspace.slug, + projectMembershipId: projectMembership.id, + isTemporary: data.temporaryAccess.isTemporary, + permissions: actions + .filter(({ allowed }) => allowed) + .map(({ action }) => ({ + action, + subject: [ProjectPermissionSub.Secrets], + conditions + })) + }); + + createNotification({ + type: "success", + text: "Successfully requested access" + }); + privilegeForm.reset(); + if (onClose) onClose(); + }; + + const handleSubmit = async (data: TSecretPermissionForm) => { + if (privilege) { + handleUpdatePrivilege(data); + } else { + handleRequestAccess(data); + } + }; + + const getAccessLabel = (exactTime = false) => { + if (isExpired) return "Access expired"; + if (!temporaryAccessField?.isTemporary) return "Permanent"; + + if (exactTime) + return `Until ${format( + new Date(temporaryAccessField.temporaryAccessEndTime || ""), + "yyyy-MM-dd HH:mm:ss" + )}`; + return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); + }; + }; return ( From 2c2afbea7a518c3bbb504df169216a0105cc5b99 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:50:16 -0700 Subject: [PATCH 120/158] Fix: Move to project slug --- .../components/AccessPolicyForm.tsx | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index 56a4bdef5..6b53e2f1e 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -32,7 +32,7 @@ type Props = { isOpen?: boolean; onToggle: (isOpen: boolean) => void; members?: TWorkspaceUser[]; - workspaceId: string; + projectSlug: string; editValues?: TAccessApprovalPolicy; }; @@ -40,7 +40,7 @@ const formSchema = z .object({ environment: z.string(), name: z.string().optional(), - secretPath: z.string().optional().nullable(), + secretPath: z.string().optional(), approvals: z.number().min(1), approvers: z.string().array().min(1) }) @@ -55,7 +55,7 @@ export const AccessPolicyForm = ({ isOpen, onToggle, members = [], - workspaceId, + projectSlug, editValues }: Props) => { const { @@ -80,10 +80,12 @@ export const AccessPolicyForm = ({ const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy(); const handleCreatePolicy = async (data: TFormSchema) => { + if (!projectSlug) return; + try { await createAccessApprovalPolicy({ ...data, - workspaceId + projectSlug }); createNotification({ type: "success", @@ -100,12 +102,14 @@ export const AccessPolicyForm = ({ }; const handleUpdatePolicy = async (data: TFormSchema) => { + if (!projectSlug) return; if (!editValues?.id) return; + try { await updateAccessApprovalPolicy({ id: editValues?.id, ...data, - workspaceId + projectSlug }); createNotification({ type: "success", @@ -154,6 +158,7 @@ export const AccessPolicyForm = ({ errorText={error?.message} > + + )} + /> + Date: Wed, 3 Apr 2024 16:50:47 -0700 Subject: [PATCH 121/158] Feat: Request access --- .../components/AccessApprovalPolicyRow.tsx | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx new file mode 100644 index 000000000..8554e928e --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx @@ -0,0 +1,146 @@ +import { useState } from "react"; +import { faCheckCircle, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + IconButton, + Input, + Td, + Tr +} from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { useUpdateAccessApprovalPolicy } from "@app/hooks/api"; +import { TAccessApprovalPolicy } from "@app/hooks/api/types"; +import { TWorkspaceUser } from "@app/hooks/api/users/types"; + +type Props = { + policy: TAccessApprovalPolicy; + members?: TWorkspaceUser[]; + projectSlug: string; + onEdit: () => void; + onDelete: () => void; +}; + +export const AccessApprovalPolicyRow = ({ + policy, + members = [], + projectSlug, + onEdit, + onDelete +}: Props) => { + const [selectedApprovers, setSelectedApprovers] = useState([]); + const { mutate: updateAccessApprovalPolicy, isLoading } = useUpdateAccessApprovalPolicy(); + const { permission } = useProjectPermission(); + + return ( + + {policy.name} + {policy.environment.slug} + {policy.secretPath || "*"} + + { + if (!isOpen) { + updateAccessApprovalPolicy( + { + projectSlug, + id: policy.id, + approvers: selectedApprovers + }, + { + onSettled: () => { + setSelectedApprovers([]); + } + } + ); + } else { + setSelectedApprovers(policy.approvers); + } + }} + > + + + + + + Select members that are allowed to 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} + +
+ + {(isAllowed) => ( + + + + )} + + + {(isAllowed) => ( + + + + )} + +
+ + + ); +}; From 1afd120e8ee2e01c054e59553be6f04e1832eae4 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:50:56 -0700 Subject: [PATCH 122/158] Feat: Request access --- .../components/AccessPolicyForm.tsx | 261 ++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx new file mode 100644 index 000000000..9cf756c14 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx @@ -0,0 +1,261 @@ +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 { createNotification } from "@app/components/notifications"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { + useCreateAccessApprovalPolicy, + useUpdateAccessApprovalPolicy +} from "@app/hooks/api/accessApproval"; +import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types"; +import { TWorkspaceUser } from "@app/hooks/api/users/types"; + +type Props = { + isOpen?: boolean; + onToggle: (isOpen: boolean) => void; + members?: TWorkspaceUser[]; + projectSlug: string; + editValues?: TAccessApprovalPolicy; +}; + +const formSchema = z + .object({ + environment: z.string(), + name: z.string().optional(), + secretPath: z.string().optional(), + approvals: z.number().min(1), + approvers: z.string().array().min(1) + }) + .refine((data) => data.approvals <= data.approvers.length, { + path: ["approvals"], + message: "The number of approvals should be lower than the number of approvers." + }); + +type TFormSchema = z.infer; + +export const AccessPolicyForm = ({ + isOpen, + onToggle, + members = [], + projectSlug, + editValues +}: Props) => { + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema), + values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined + }); + const { currentWorkspace } = useWorkspace(); + + const environments = currentWorkspace?.environments || []; + useEffect(() => { + if (!isOpen) reset({}); + }, [isOpen]); + + const isEditMode = Boolean(editValues); + + const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy(); + const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy(); + + const handleCreatePolicy = async (data: TFormSchema) => { + try { + await createAccessApprovalPolicy({ + ...data, + projectSlug + }); + 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 updateAccessApprovalPolicy({ + id: editValues?.id, + ...data, + projectSlug + }); + 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 are allowed to 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} + + ); + })} + + + + )} + /> + ( + + field.onChange(parseInt(el.target.value, 10))} + /> + + )} + /> +
+ + +
+ +
+
+ ); +}; From 3bb50b235d223eccc5391b558eb3e3f092ad338a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:53:02 -0700 Subject: [PATCH 123/158] Update generate-schema-types.ts --- backend/scripts/generate-schema-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 28b736152..8c913991f 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -5,7 +5,7 @@ import knex from "knex"; import { writeFileSync } from "fs"; dotenv.config({ - path: path.join(__dirname, "../../.env") + path: path.join(__dirname, "../../.env.migration") }); const db = knex({ From 3f0f45e85388fc796ac58ff4572c279d65c6b9b2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:13:23 -0700 Subject: [PATCH 124/158] Update SpecificPrivilegeSection.tsx --- .../MemberRoleForm/SpecificPrivilegeSection.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx index dd09226eb..bb330aa4d 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -388,14 +388,13 @@ export const SpecificPrivilegeSecretForm = ({ if (isExpired) return "Access expired"; if (!temporaryAccessField?.isTemporary) return "Permanent"; - if (exactTime) + if (exactTime) { return `Until ${format( new Date(temporaryAccessField.temporaryAccessEndTime || ""), "yyyy-MM-dd HH:mm:ss" )}`; + } return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); - }; - }; return ( From a3b4b650d1b1949bef7d39fae7d150766baa7371 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:41:17 -0700 Subject: [PATCH 125/158] Removed unused parameter --- .../ee/routes/v1/access-approval-request-router.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 696c8b76f..2a6142c88 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -1,9 +1,7 @@ -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas"; import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; -import { alphaNumericNanoId } from "@app/lib/nanoid"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -13,16 +11,6 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv method: "POST", schema: { body: z.object({ - slug: z - .string() - .min(1) - .max(60) - .trim() - .default(`requested-privilege-${slugify(alphaNumericNanoId(12))}`) - .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }), permissions: z.any().array(), isTemporary: z.boolean(), temporaryRange: z.string().optional() From 27447ddc88d2f7f5b9b896f2b5b7b01044a4879a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:43:45 -0700 Subject: [PATCH 126/158] Update licence-fns.ts --- backend/src/ee/services/license/licence-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 492d09ec3..189a3c4e0 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ status: null, trial_end: null, has_used_trial: true, - secretApproval: true, + secretApproval: false, secretRotation: true }); From 923bf0204623d8d193f414a2b4ab5096451a4201 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 3 Apr 2024 20:24:33 -0700 Subject: [PATCH 127/158] style changes --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 235737d4a..6479117b4 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -317,7 +317,7 @@ export const AccessApprovalRequest = ({
)} {!!filteredRequests?.length && - filteredRequests?.map((request) => { + requests?.map((request) => { const details = generateRequestDetails(request); return ( From 121902e51f761330dd63b76fab88f7dc5719d964 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:29:20 -0700 Subject: [PATCH 128/158] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 6479117b4..235737d4a 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -317,7 +317,7 @@ export const AccessApprovalRequest = ({
)} {!!filteredRequests?.length && - requests?.map((request) => { + filteredRequests?.map((request) => { const details = generateRequestDetails(request); return ( From 350dd97b98e9ee906608962a2f9ff72808f966a3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:33:58 -0700 Subject: [PATCH 129/158] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 235737d4a..4896c9a3e 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -311,7 +311,7 @@ export const AccessApprovalRequest = ({
- {filteredRequests?.length === 0 && ( + {filteredRequests?.length === 0 && ( //
From 5117f5d3c10ce86dc6ddfc6f75092c4b6fa8328e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:34:11 -0700 Subject: [PATCH 130/158] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 4896c9a3e..235737d4a 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -311,7 +311,7 @@ export const AccessApprovalRequest = ({
- {filteredRequests?.length === 0 && ( // + {filteredRequests?.length === 0 && (
From 7d1dff9e5a19ec9e6cd4bf5ce7e14c67801f1e86 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:00:14 -0700 Subject: [PATCH 131/158] Fix: Security vulnurbility making it possible to spoof env & secret path requested. --- backend/src/ee/routes/v1/access-approval-request-router.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 2a6142c88..4b173cfa7 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -16,9 +16,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv temporaryRange: z.string().optional() }), querystring: z.object({ - projectSlug: z.string().trim(), - secretPath: z.string().trim(), - envSlug: z.string().trim() + projectSlug: z.string().trim() }), response: { 200: z.object({ @@ -33,9 +31,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, permissions: req.body.permissions, - envSlug: req.query.envSlug, actorOrgId: req.permission.orgId, - secretPath: req.query.secretPath, projectSlug: req.query.projectSlug, temporaryRange: req.body.temporaryRange, isTemporary: req.body.isTemporary From 038fe3508c5054608b85c5e03871e2e12a1fe51d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:03:19 -0700 Subject: [PATCH 132/158] Removed unnessecary types --- .../MemberRoleForm/SpecificPrivilegeSection.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx index bb330aa4d..88ff41cf8 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -269,7 +269,6 @@ export const SpecificPrivilegeSecretForm = ({ conditions.secretPath = { $glob: data.secretPath }; } await requestAccess.mutateAsync({ - ...data, ...(data.temporaryAccess.isTemporary && { temporaryRange: data.temporaryAccess.temporaryRange }), @@ -354,10 +353,7 @@ export const SpecificPrivilegeSecretForm = ({ temporaryRange: data.temporaryAccess.temporaryRange, temporaryMode: "relative" }), - envSlug: data.environmentSlug, - secretPath: data.secretPath, projectSlug: currentWorkspace.slug, - projectMembershipId: projectMembership.id, isTemporary: data.temporaryAccess.isTemporary, permissions: actions .filter(({ allowed }) => allowed) @@ -388,7 +384,7 @@ export const SpecificPrivilegeSecretForm = ({ if (isExpired) return "Access expired"; if (!temporaryAccessField?.isTemporary) return "Permanent"; - if (exactTime) { + if (exactTime && !policies) { return `Until ${format( new Date(temporaryAccessField.temporaryAccessEndTime || ""), "yyyy-MM-dd HH:mm:ss" From e8e1d46f0e03cdf9178faffe1a3e5f532cf29781 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:03:29 -0700 Subject: [PATCH 133/158] Capitalization --- .../AccessApprovalPolicyList/components/AccessPolicyForm.tsx | 2 +- .../AccessApprovalRequest/components/AccessPolicyForm.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index 6b53e2f1e..dd51ea53d 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -135,7 +135,7 @@ export const AccessPolicyForm = ({ return ( - +
- + Date: Thu, 4 Apr 2024 01:03:44 -0700 Subject: [PATCH 134/158] Style: Fix styling --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 235737d4a..b9d55ff01 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -26,7 +26,6 @@ import { import { Badge } from "@app/components/v2/Badge"; import { ProjectPermissionActions, - ProjectPermissionSub, useProjectPermission, useSubscription, useWorkspace From c66476e2b4b2d15805e2d353f4bcc47debdd8748 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 12:44:49 -0700 Subject: [PATCH 135/158] Fix: Multiple approvers acceptance bug --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index b9d55ff01..235737d4a 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -26,6 +26,7 @@ import { import { Badge } from "@app/components/v2/Badge"; import { ProjectPermissionActions, + ProjectPermissionSub, useProjectPermission, useSubscription, useWorkspace From cb505d15258f044c6b27bf6b7f4cf08ec048348a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 29 Mar 2024 16:24:15 +0100 Subject: [PATCH 136/158] Draft --- backend/src/ee/services/license/licence-fns.ts | 2 +- .../src/components/permissions/PermissionDeniedBanner.tsx | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 189a3c4e0..492d09ec3 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ status: null, trial_end: null, has_used_trial: true, - secretApproval: false, + secretApproval: true, secretRotation: true }); diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx index b3c7a4f53..f9707d634 100644 --- a/frontend/src/components/permissions/PermissionDeniedBanner.tsx +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -3,6 +3,8 @@ import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; +import { Button } from "../v2"; + type Props = { containerClassName?: string; className?: string; @@ -32,6 +34,9 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children )}
+
); From db0b4a5ad13df058774f04292564fdfa3fea0178 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 1 Apr 2024 09:49:22 -0700 Subject: [PATCH 137/158] Feat: Request access --- backend/scripts/generate-schema-types.ts | 2 +- .../access-approval-policy/access-approval-policy-service.ts | 2 -- frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx | 4 ++++ .../AccessApprovalPolicyList/components/AccessPolicyForm.tsx | 3 --- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 8c913991f..28b736152 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -5,7 +5,7 @@ import knex from "knex"; import { writeFileSync } from "fs"; dotenv.config({ - path: path.join(__dirname, "../../.env.migration") + path: path.join(__dirname, "../../.env") }); const db = knex({ diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index f14380869..50e1b04b4 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -155,7 +155,6 @@ export const accessApprovalPolicyServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { @@ -179,7 +178,6 @@ export const accessApprovalPolicyServiceFactory = ({ ); await verifyApprovers({ - projectId: accessApprovalPolicy.projectId, orgId: actorOrgId, envSlug: accessApprovalPolicy.environment.slug, secretPath: doc.secretPath!, diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 9c273848f..b3e0b62ca 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -65,6 +65,10 @@ export const SecretApprovalPage = () => { + + + + ); diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index dd51ea53d..081556041 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -177,7 +177,6 @@ export const AccessPolicyForm = ({ /> ( @@ -187,8 +186,6 @@ export const AccessPolicyForm = ({ /> ( Date: Wed, 3 Apr 2024 16:37:49 -0700 Subject: [PATCH 138/158] Feat: Request access (new routes) --- backend/src/ee/routes/v1/access-approval-request-router.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 4b173cfa7..104f32665 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -2,10 +2,6 @@ import { z } from "zod"; import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas"; import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; -import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AuthMode } from "@app/services/auth/auth-type"; - -export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => { server.route({ url: "/", method: "POST", From 7b8af89beefe33533bb469242bfe6e299e4ceb53 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:39:15 -0700 Subject: [PATCH 139/158] Fix: Validate approvers access --- .../access-approval-policy/access-approval-policy-service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index 50e1b04b4..ed812fba6 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -155,6 +155,7 @@ export const accessApprovalPolicyServiceFactory = ({ actorAuthMethod, actorOrgId ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { From 801c0c5ada2204c9d95c4046a3b32aadff94ceb6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:43:48 -0700 Subject: [PATCH 140/158] Fix: Remove redundant code --- .../src/components/permissions/PermissionDeniedBanner.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx index f9707d634..b3c7a4f53 100644 --- a/frontend/src/components/permissions/PermissionDeniedBanner.tsx +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -3,8 +3,6 @@ import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { Button } from "../v2"; - type Props = { containerClassName?: string; className?: string; @@ -34,9 +32,6 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children )} - ); From e53d40f0e58cf364ce7c32f78c9497676758a0e0 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:51:21 -0700 Subject: [PATCH 141/158] Update SecretApprovalPage.tsx --- frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index b3e0b62ca..7b679a029 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -67,7 +67,7 @@ export const SecretApprovalPage = () => { - + From 0580f37c5ec4be0644567040e20ec2e20d5747e7 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:53:02 -0700 Subject: [PATCH 142/158] Update generate-schema-types.ts --- backend/scripts/generate-schema-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 28b736152..8c913991f 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -5,7 +5,7 @@ import knex from "knex"; import { writeFileSync } from "fs"; dotenv.config({ - path: path.join(__dirname, "../../.env") + path: path.join(__dirname, "../../.env.migration") }); const db = knex({ From 9198eb5fbaf64e7cf5f7e7023709530c1305a5fa Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:43:45 -0700 Subject: [PATCH 143/158] Update licence-fns.ts --- backend/src/ee/services/license/licence-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 492d09ec3..189a3c4e0 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ status: null, trial_end: null, has_used_trial: true, - secretApproval: true, + secretApproval: false, secretRotation: true }); From 1a2d8e96f3da6e88cbbf554a6293a4071349bcb0 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 3 Apr 2024 20:24:33 -0700 Subject: [PATCH 144/158] style changes --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 235737d4a..6479117b4 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -317,7 +317,7 @@ export const AccessApprovalRequest = ({ )} {!!filteredRequests?.length && - filteredRequests?.map((request) => { + requests?.map((request) => { const details = generateRequestDetails(request); return ( From 949d210263cdf2867ee66c7fb6a72965fc2e97b7 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:29:20 -0700 Subject: [PATCH 145/158] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 6479117b4..235737d4a 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -317,7 +317,7 @@ export const AccessApprovalRequest = ({ )} {!!filteredRequests?.length && - requests?.map((request) => { + filteredRequests?.map((request) => { const details = generateRequestDetails(request); return ( From 6e209bf099ddb39cecbff3e08acba0618984b468 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:33:58 -0700 Subject: [PATCH 146/158] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 235737d4a..4896c9a3e 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -311,7 +311,7 @@ export const AccessApprovalRequest = ({
- {filteredRequests?.length === 0 && ( + {filteredRequests?.length === 0 && ( //
From 29b26e3158983138b33b1a79e95f97438c4cf65b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:34:11 -0700 Subject: [PATCH 147/158] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 4896c9a3e..235737d4a 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -311,7 +311,7 @@ export const AccessApprovalRequest = ({
- {filteredRequests?.length === 0 && ( // + {filteredRequests?.length === 0 && (
From 56c8b4f5e56b9ac6c5812d85a30697d815159035 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:03:19 -0700 Subject: [PATCH 148/158] Removed unnessecary types --- .../MemberRoleForm/SpecificPrivilegeSection.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx index 88ff41cf8..9252f09e6 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -348,10 +348,7 @@ export const SpecificPrivilegeSecretForm = ({ await requestAccess.mutateAsync({ ...data, ...(data.temporaryAccess.isTemporary && { - temporaryAccessStartTime: data.temporaryAccess.temporaryAccessStartTime, - temporaryAccessEndTime: data.temporaryAccess.temporaryAccessEndTime, - temporaryRange: data.temporaryAccess.temporaryRange, - temporaryMode: "relative" + temporaryRange: data.temporaryAccess.temporaryRange }), projectSlug: currentWorkspace.slug, isTemporary: data.temporaryAccess.isTemporary, From ce1ad6f32e1cf16dd856e50095d7774ea4552c79 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 20:14:02 -0700 Subject: [PATCH 149/158] Fix: Rebase errors --- .../v1/access-approval-request-router.ts | 4 + .../access-approval-policy-service.ts | 1 + .../SpecificPrivilegeSection.tsx | 77 ------------------- .../components/AccessPolicyForm.tsx | 2 + 4 files changed, 7 insertions(+), 77 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 104f32665..4b173cfa7 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -2,6 +2,10 @@ import { z } from "zod"; import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas"; import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => { server.route({ url: "/", method: "POST", diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index ed812fba6..f14380869 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -179,6 +179,7 @@ export const accessApprovalPolicyServiceFactory = ({ ); await verifyApprovers({ + projectId: accessApprovalPolicy.projectId, orgId: actorOrgId, envSlug: accessApprovalPolicy.environment.slug, secretPath: doc.secretPath!, diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx index 9252f09e6..6fb97063b 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -237,83 +237,6 @@ export const SpecificPrivilegeSecretForm = ({ } }; - // This is used for requesting access additional privileges, not directly creating a privilege! - const handleRequestAccess = async (data: TSecretPermissionForm) => { - if (!policies) return; - if (!currentWorkspace) { - createNotification({ - type: "error", - text: "No workspace found.", - title: "Error" - }); - return; - } - - if (!data.secretPath) { - createNotification({ - type: "error", - text: "Please select a secret path", - title: "Error" - }); - return; - } - - const actions = [ - { action: ProjectPermissionActions.Read, allowed: data.read }, - { action: ProjectPermissionActions.Create, allowed: data.create }, - { action: ProjectPermissionActions.Delete, allowed: data.delete }, - { action: ProjectPermissionActions.Edit, allowed: data.edit } - ]; - const conditions: Record = { environment: data.environmentSlug }; - if (data.secretPath) { - conditions.secretPath = { $glob: data.secretPath }; - } - await requestAccess.mutateAsync({ - ...(data.temporaryAccess.isTemporary && { - temporaryRange: data.temporaryAccess.temporaryRange - }), - projectSlug: currentWorkspace.slug, - isTemporary: data.temporaryAccess.isTemporary, - permissions: actions - .filter(({ allowed }) => allowed) - .map(({ action }) => ({ - action, - subject: [ProjectPermissionSub.Secrets], - conditions - })) - }); - - createNotification({ - type: "success", - text: "Successfully requested access" - }); - privilegeForm.reset(); - if (onClose) onClose(); - }; - - const handleSubmit = async (data: TSecretPermissionForm) => { - if (privilege) { - handleUpdatePrivilege(data); - } else { - handleRequestAccess(data); - } - }; - - const getAccessLabel = (exactTime = false) => { - if (isExpired) return "Access expired"; - if (!temporaryAccessField?.isTemporary) return "Permanent"; - - if (exactTime && !policies) { - return `Until ${format( - new Date(temporaryAccessField.temporaryAccessEndTime || ""), - "yyyy-MM-dd HH:mm:ss" - )}`; - } - return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); - }; - - }; - // This is used for requesting access additional privileges, not directly creating a privilege! const handleRequestAccess = async (data: TSecretPermissionForm) => { if (!policies) return; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index 081556041..a07acb584 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -186,6 +186,8 @@ export const AccessPolicyForm = ({ /> ( Date: Thu, 4 Apr 2024 21:00:02 -0700 Subject: [PATCH 150/158] Update SecretApprovalPage.tsx --- frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 7b679a029..9c273848f 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -65,10 +65,6 @@ export const SecretApprovalPage = () => { - - - -
); From 4a0ccbe69e990d038772f68016d96b99931b6ebc Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 21:12:05 -0700 Subject: [PATCH 151/158] Fixed bugs --- .../AccessApprovalPolicyList/components/AccessPolicyForm.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index a07acb584..dd51ea53d 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -177,6 +177,7 @@ export const AccessPolicyForm = ({ /> ( @@ -186,8 +187,8 @@ export const AccessPolicyForm = ({ /> ( Date: Thu, 4 Apr 2024 22:14:46 -0700 Subject: [PATCH 152/158] Migration improvements --- .../20240330075122_access-approval-policy.ts | 1 + .../20240401173320_access_approval_requests.ts | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index e6d47d864..2fc11bca9 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -35,6 +35,7 @@ export async function up(knex: Knex): Promise { export async function down(knex: Knex): Promise { await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover); await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); + await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); } diff --git a/backend/src/db/migrations/20240401173320_access_approval_requests.ts b/backend/src/db/migrations/20240401173320_access_approval_requests.ts index 901be9a78..c03287b57 100644 --- a/backend/src/db/migrations/20240401173320_access_approval_requests.ts +++ b/backend/src/db/migrations/20240401173320_access_approval_requests.ts @@ -43,9 +43,16 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { + const reviewerTableExists = await knex.schema.hasTable(TableName.AccessApprovalRequestReviewer); + const requestTableExists = await knex.schema.hasTable(TableName.AccessApprovalRequest); + await knex.schema.dropTableIfExists(TableName.AccessApprovalRequestReviewer); await knex.schema.dropTableIfExists(TableName.AccessApprovalRequest); - await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); - await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); + if (reviewerTableExists) { + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); + } + if (requestTableExists) { + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); + } } From fb6c4acf3199b48c5c3c60097a0db0e654ee9311 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:59:01 -0700 Subject: [PATCH 153/158] Delete access-approval-request-secret-dal.ts --- .../access-approval-request-secret-dal.ts | 230 ------------------ 1 file changed, 230 deletions(-) delete mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts deleted file mode 100644 index d458d58ff..000000000 --- a/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { Knex } from "knex"; - -import { TDbClient } from "@app/db"; -import { - SecretApprovalRequestsSecretsSchema, - TableName, - TSecretApprovalRequestsSecrets, - TSecretTags -} from "@app/db/schemas"; -import { BadRequestError, DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; - -export type TAccessApprovalRequestSecretDALFactory = ReturnType; - -export const accessApprovalRequestSecretDALFactory = (db: TDbClient) => { - const accessApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret); - const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag); - - const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => { - try { - const existingApprovalSecrets = await accessApprovalRequestSecretOrm.find( - { - $in: { - id: data.map((el) => el.id) - } - }, - { tx } - ); - - if (existingApprovalSecrets.length !== data.length) { - throw new BadRequestError({ message: "Some of the secret approvals do not exist" }); - } - - if (data.length === 0) return []; - - const updatedApprovalSecrets = await (tx || db)(TableName.SecretApprovalRequestSecret) - .insert(data) - .onConflict("id") // this will cause a conflict then merge the data - .merge() // Merge the data with the existing data - .returning("*"); - - return updatedApprovalSecrets; - } catch (error) { - throw new DatabaseError({ error, name: "bulk update secret" }); - } - }; - - const findByRequestId = async (requestId: string, tx?: Knex) => { - try { - const doc = await (tx || db)({ - secVerTag: TableName.SecretTag - }) - .from(TableName.SecretApprovalRequestSecret) - .where({ requestId }) - .leftJoin( - TableName.SecretApprovalRequestSecretTag, - `${TableName.SecretApprovalRequestSecret}.id`, - `${TableName.SecretApprovalRequestSecretTag}.secretId` - ) - .leftJoin(TableName.SecretTag, `${TableName.SecretApprovalRequestSecretTag}.tagId`, `${TableName.SecretTag}.id`) - .leftJoin(TableName.Secret, `${TableName.SecretApprovalRequestSecret}.secretId`, `${TableName.Secret}.id`) - .leftJoin( - TableName.SecretVersion, - `${TableName.SecretVersion}.id`, - `${TableName.SecretApprovalRequestSecret}.secretVersion` - ) - .leftJoin( - TableName.SecretVersionTag, - `${TableName.SecretVersionTag}.${TableName.SecretVersion}Id`, - `${TableName.SecretVersion}.id` - ) - .leftJoin( - db.ref(TableName.SecretTag).as("secVerTag"), - `${TableName.SecretVersionTag}.${TableName.SecretTag}Id`, - db.ref("id").withSchema("secVerTag") - ) - .select(selectAllTableCols(TableName.SecretApprovalRequestSecret)) - .select({ - secVerTagId: "secVerTag.id", - secVerTagColor: "secVerTag.color", - secVerTagSlug: "secVerTag.slug", - secVerTagName: "secVerTag.name" - }) - .select( - db.ref("id").withSchema(TableName.SecretTag).as("tagId"), - db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"), - db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), - db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), - db.ref("name").withSchema(TableName.SecretTag).as("tagName") - ) - .select( - db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"), - db.ref("version").withSchema(TableName.Secret).as("orgSecVersion"), - db.ref("secretKeyIV").withSchema(TableName.Secret).as("orgSecKeyIV"), - db.ref("secretKeyTag").withSchema(TableName.Secret).as("orgSecKeyTag"), - db.ref("secretKeyCiphertext").withSchema(TableName.Secret).as("orgSecKeyCiphertext"), - db.ref("secretValueIV").withSchema(TableName.Secret).as("orgSecValueIV"), - db.ref("secretValueTag").withSchema(TableName.Secret).as("orgSecValueTag"), - db.ref("secretValueCiphertext").withSchema(TableName.Secret).as("orgSecValueCiphertext"), - db.ref("secretCommentIV").withSchema(TableName.Secret).as("orgSecCommentIV"), - db.ref("secretCommentTag").withSchema(TableName.Secret).as("orgSecCommentTag"), - db.ref("secretCommentCiphertext").withSchema(TableName.Secret).as("orgSecCommentCiphertext") - ) - .select( - db.ref("version").withSchema(TableName.SecretVersion).as("secVerVersion"), - db.ref("secretKeyIV").withSchema(TableName.SecretVersion).as("secVerKeyIV"), - db.ref("secretKeyTag").withSchema(TableName.SecretVersion).as("secVerKeyTag"), - db.ref("secretKeyCiphertext").withSchema(TableName.SecretVersion).as("secVerKeyCiphertext"), - db.ref("secretValueIV").withSchema(TableName.SecretVersion).as("secVerValueIV"), - db.ref("secretValueTag").withSchema(TableName.SecretVersion).as("secVerValueTag"), - db.ref("secretValueCiphertext").withSchema(TableName.SecretVersion).as("secVerValueCiphertext"), - db.ref("secretCommentIV").withSchema(TableName.SecretVersion).as("secVerCommentIV"), - db.ref("secretCommentTag").withSchema(TableName.SecretVersion).as("secVerCommentTag"), - db.ref("secretCommentCiphertext").withSchema(TableName.SecretVersion).as("secVerCommentCiphertext") - ); - const formatedDoc = sqlNestRelationships({ - data: doc, - key: "id", - parentMapper: (data) => SecretApprovalRequestsSecretsSchema.omit({ secretVersion: true }).parse(data), - childrenMapper: [ - { - key: "tagJnId", - label: "tags" as const, - mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({ - id, - name, - slug, - color - }) - }, - { - key: "secretId", - label: "secret" as const, - mapper: ({ - orgSecKeyIV, - orgSecKeyTag, - orgSecValueIV, - orgSecVersion, - orgSecValueTag, - orgSecCommentIV, - orgSecBlindIndex, - orgSecCommentTag, - orgSecKeyCiphertext, - orgSecValueCiphertext, - orgSecCommentCiphertext, - secretId - }) => - secretId - ? { - id: secretId, - version: orgSecVersion, - secretBlindIndex: orgSecBlindIndex, - secretKeyIV: orgSecKeyIV, - secretKeyTag: orgSecKeyTag, - secretKeyCiphertext: orgSecKeyCiphertext, - secretValueIV: orgSecValueIV, - secretValueTag: orgSecValueTag, - secretValueCiphertext: orgSecValueCiphertext, - secretCommentIV: orgSecCommentIV, - secretCommentTag: orgSecCommentTag, - secretCommentCiphertext: orgSecCommentCiphertext - } - : undefined - }, - { - key: "secretVersion", - label: "secretVersion" as const, - mapper: ({ - secVerCommentIV, - secVerCommentCiphertext, - secVerCommentTag, - secVerValueCiphertext, - secVerKeyIV, - secVerKeyTag, - secVerValueIV, - secretVersion, - secVerValueTag, - secVerKeyCiphertext, - secVerVersion - }) => - secretVersion - ? { - version: secVerVersion, - id: secretVersion, - secretKeyIV: secVerKeyIV, - secretKeyTag: secVerKeyTag, - secretKeyCiphertext: secVerKeyCiphertext, - secretValueIV: secVerValueIV, - secretValueTag: secVerValueTag, - secretValueCiphertext: secVerValueCiphertext, - secretCommentIV: secVerCommentIV, - secretCommentTag: secVerCommentTag, - secretCommentCiphertext: secVerCommentCiphertext - } - : undefined, - childrenMapper: [ - { - key: "secVerTagId", - label: "tags" as const, - mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({ - // eslint-disable-next-line - id, - // eslint-disable-next-line - name, - // eslint-disable-next-line - slug, - // eslint-disable-next-line - color - }) - } - ] - } - ] - }); - return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({ - ...el, - secret: secret?.[0], - secretVersion: secretVersion?.[0] - })); - } catch (error) { - throw new DatabaseError({ error, name: "FindByRequestId" }); - } - }; - return { - ...accessApprovalRequestSecretOrm, - findByRequestId, - bulkUpdateNoVersionIncrement, - insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany - }; -}; From af0d31db2cfe32bfc6fd26f07bcd8ccbc59228b9 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:18:02 -0700 Subject: [PATCH 154/158] Fix: Improved migrations --- .../20240401173320_access_approval_requests.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/backend/src/db/migrations/20240401173320_access_approval_requests.ts b/backend/src/db/migrations/20240401173320_access_approval_requests.ts index c03287b57..901be9a78 100644 --- a/backend/src/db/migrations/20240401173320_access_approval_requests.ts +++ b/backend/src/db/migrations/20240401173320_access_approval_requests.ts @@ -43,16 +43,9 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { - const reviewerTableExists = await knex.schema.hasTable(TableName.AccessApprovalRequestReviewer); - const requestTableExists = await knex.schema.hasTable(TableName.AccessApprovalRequest); - await knex.schema.dropTableIfExists(TableName.AccessApprovalRequestReviewer); await knex.schema.dropTableIfExists(TableName.AccessApprovalRequest); - if (reviewerTableExists) { - await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); - } - if (requestTableExists) { - await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); - } + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); } From a579598b6dc7ea9d75348cf1797caba64088159b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:19:43 -0700 Subject: [PATCH 155/158] Chore: Moved verifyApprovers --- .../access-approval-policy/access-approval-policy-service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index f14380869..51a51abb5 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -54,7 +54,6 @@ export const accessApprovalPolicyServiceFactory = ({ if (approvals > approvers.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); - if (!secretPath) throw new BadRequestError({ message: "Secret path is required" }); const { permission } = await permissionService.getProjectPermission( actor, From 9c2591f3a61108b1cd57fe5a8b3bcd64842bfa3e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:20:18 -0700 Subject: [PATCH 156/158] Fix: Moved Divider to v2 --- frontend/src/components/basic/Divider.tsx | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 frontend/src/components/basic/Divider.tsx diff --git a/frontend/src/components/basic/Divider.tsx b/frontend/src/components/basic/Divider.tsx deleted file mode 100644 index f1a570d7b..000000000 --- a/frontend/src/components/basic/Divider.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { twMerge } from "tailwind-merge"; - -interface IProps { - className?: string; -} - -const Divider = ({ className }: IProps): JSX.Element => { - return ( -
- - ); -}; - -export default Divider; From 95c1fff7d3a77e9e7af09f86a31e3aab12ae0d98 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:21:57 -0700 Subject: [PATCH 157/158] Chore: Remove unused files --- .../components/AccessPolicyForm.tsx | 266 ------------------ .../components/AccessApprovalPolicyRow.tsx | 146 ---------- .../components/AccessPolicyForm.tsx | 261 ----------------- 3 files changed, 673 deletions(-) delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx deleted file mode 100644 index dd51ea53d..000000000 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ /dev/null @@ -1,266 +0,0 @@ -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 { createNotification } from "@app/components/notifications"; -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, - FormControl, - Input, - Modal, - ModalContent, - Select, - SelectItem -} from "@app/components/v2"; -import { useWorkspace } from "@app/context"; -import { - useCreateAccessApprovalPolicy, - useUpdateAccessApprovalPolicy -} from "@app/hooks/api/accessApproval"; -import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types"; -import { TWorkspaceUser } from "@app/hooks/api/users/types"; - -type Props = { - isOpen?: boolean; - onToggle: (isOpen: boolean) => void; - members?: TWorkspaceUser[]; - projectSlug: string; - editValues?: TAccessApprovalPolicy; -}; - -const formSchema = z - .object({ - environment: z.string(), - name: z.string().optional(), - secretPath: z.string().optional(), - approvals: z.number().min(1), - approvers: z.string().array().min(1) - }) - .refine((data) => data.approvals <= data.approvers.length, { - path: ["approvals"], - message: "The number of approvals should be lower than the number of approvers." - }); - -type TFormSchema = z.infer; - -export const AccessPolicyForm = ({ - isOpen, - onToggle, - members = [], - projectSlug, - editValues -}: Props) => { - const { - control, - handleSubmit, - reset, - formState: { isSubmitting } - } = useForm({ - resolver: zodResolver(formSchema), - values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined - }); - const { currentWorkspace } = useWorkspace(); - - const environments = currentWorkspace?.environments || []; - useEffect(() => { - if (!isOpen) reset({}); - }, [isOpen]); - - const isEditMode = Boolean(editValues); - - const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy(); - const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy(); - - const handleCreatePolicy = async (data: TFormSchema) => { - if (!projectSlug) return; - - try { - await createAccessApprovalPolicy({ - ...data, - projectSlug - }); - 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 (!projectSlug) return; - if (!editValues?.id) return; - - try { - await updateAccessApprovalPolicy({ - id: editValues?.id, - ...data, - projectSlug - }); - 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 are allowed to 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} - - ); - })} - - - - )} - /> - ( - - field.onChange(parseInt(el.target.value, 10))} - /> - - )} - /> -
- - -
- -
-
- ); -}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx deleted file mode 100644 index 8554e928e..000000000 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { useState } from "react"; -import { faCheckCircle, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { ProjectPermissionCan } from "@app/components/permissions"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, - IconButton, - Input, - Td, - Tr -} from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; -import { useUpdateAccessApprovalPolicy } from "@app/hooks/api"; -import { TAccessApprovalPolicy } from "@app/hooks/api/types"; -import { TWorkspaceUser } from "@app/hooks/api/users/types"; - -type Props = { - policy: TAccessApprovalPolicy; - members?: TWorkspaceUser[]; - projectSlug: string; - onEdit: () => void; - onDelete: () => void; -}; - -export const AccessApprovalPolicyRow = ({ - policy, - members = [], - projectSlug, - onEdit, - onDelete -}: Props) => { - const [selectedApprovers, setSelectedApprovers] = useState([]); - const { mutate: updateAccessApprovalPolicy, isLoading } = useUpdateAccessApprovalPolicy(); - const { permission } = useProjectPermission(); - - return ( - - {policy.name} - {policy.environment.slug} - {policy.secretPath || "*"} - - { - if (!isOpen) { - updateAccessApprovalPolicy( - { - projectSlug, - id: policy.id, - approvers: selectedApprovers - }, - { - onSettled: () => { - setSelectedApprovers([]); - } - } - ); - } else { - setSelectedApprovers(policy.approvers); - } - }} - > - - - - - - Select members that are allowed to 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} - -
- - {(isAllowed) => ( - - - - )} - - - {(isAllowed) => ( - - - - )} - -
- - - ); -}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx deleted file mode 100644 index 7e88da100..000000000 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx +++ /dev/null @@ -1,261 +0,0 @@ -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 { createNotification } from "@app/components/notifications"; -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, - FormControl, - Input, - Modal, - ModalContent, - Select, - SelectItem -} from "@app/components/v2"; -import { useWorkspace } from "@app/context"; -import { - useCreateAccessApprovalPolicy, - useUpdateAccessApprovalPolicy -} from "@app/hooks/api/accessApproval"; -import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types"; -import { TWorkspaceUser } from "@app/hooks/api/users/types"; - -type Props = { - isOpen?: boolean; - onToggle: (isOpen: boolean) => void; - members?: TWorkspaceUser[]; - projectSlug: string; - editValues?: TAccessApprovalPolicy; -}; - -const formSchema = z - .object({ - environment: z.string(), - name: z.string().optional(), - secretPath: z.string().optional(), - approvals: z.number().min(1), - approvers: z.string().array().min(1) - }) - .refine((data) => data.approvals <= data.approvers.length, { - path: ["approvals"], - message: "The number of approvals should be lower than the number of approvers." - }); - -type TFormSchema = z.infer; - -export const AccessPolicyForm = ({ - isOpen, - onToggle, - members = [], - projectSlug, - editValues -}: Props) => { - const { - control, - handleSubmit, - reset, - formState: { isSubmitting } - } = useForm({ - resolver: zodResolver(formSchema), - values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined - }); - const { currentWorkspace } = useWorkspace(); - - const environments = currentWorkspace?.environments || []; - useEffect(() => { - if (!isOpen) reset({}); - }, [isOpen]); - - const isEditMode = Boolean(editValues); - - const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy(); - const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy(); - - const handleCreatePolicy = async (data: TFormSchema) => { - try { - await createAccessApprovalPolicy({ - ...data, - projectSlug - }); - 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 updateAccessApprovalPolicy({ - id: editValues?.id, - ...data, - projectSlug - }); - 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 are allowed to 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} - - ); - })} - - - - )} - /> - ( - - field.onChange(parseInt(el.target.value, 10))} - /> - - )} - /> -
- - -
- -
-
- ); -}; From 9a585ad9309ee89d12f83c55c2e965e732ef6db1 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 6 May 2024 11:24:04 +0200 Subject: [PATCH 158/158] Fix: Rebase error --- .../src/db/migrations/20240330075122_access-approval-policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index 2fc11bca9..feeecd25b 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -12,7 +12,6 @@ export async function up(knex: Knex): Promise { t.string("secretPath"); t.uuid("envId").notNullable(); - t.string("secretPath"); t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); t.timestamps(true, true, true); }); @@ -24,6 +23,7 @@ export async function up(knex: Knex): Promise { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.uuid("approverId").notNullable(); t.foreign("approverId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE"); + t.uuid("policyId").notNullable(); t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE"); t.timestamps(true, true, true);