From 7cd83ad945da68a9e66318960950441754b56533 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 8 Oct 2024 17:36:56 +0530 Subject: [PATCH] feat: added lease permission for dynamic secret --- .../dynamic-secret-lease-service.ts | 15 ++-- .../dynamic-secret/dynamic-secret-service.ts | 21 +++--- .../services/permission/project-permission.ts | 35 +++++++--- .../src/server/routes/v1/dashboard-router.ts | 7 +- .../ProjectPermissionContext/index.tsx | 1 + .../context/ProjectPermissionContext/types.ts | 10 ++- frontend/src/context/index.tsx | 1 + .../ProjectRoleModifySection.utils.ts | 69 ++++++++++++++----- .../views/SecretMainPage/SecretMainPage.tsx | 3 +- .../components/ActionBar/ActionBar.tsx | 11 ++- .../DynamicSecretLease.tsx | 24 +++++-- .../DynamicSecretListView.tsx | 36 ++++++---- 12 files changed, 166 insertions(+), 67 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index fe133520d..262874d8c 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -4,7 +4,10 @@ import ms from "ms"; import { SecretKeyEncoding } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionDynamicSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -73,7 +76,7 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); @@ -146,7 +149,7 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); @@ -220,7 +223,7 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); @@ -285,7 +288,7 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); @@ -321,7 +324,7 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 5de76feed..d207df4ce 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -3,7 +3,10 @@ import { ForbiddenError, subject } from "@casl/ability"; import { SecretKeyEncoding } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionDynamicSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrderByDirection } from "@app/lib/types"; @@ -77,7 +80,7 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionDynamicSecretActions.Create, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); @@ -146,7 +149,7 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionDynamicSecretActions.Edit, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); @@ -225,7 +228,7 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionDynamicSecretActions.Edit, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); @@ -282,7 +285,7 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionDynamicSecretActions.Edit, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); @@ -328,7 +331,7 @@ export const dynamicSecretServiceFactory = ({ // verify user has access to each env in request environmentSlugs.forEach((environmentSlug) => ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ) ); @@ -364,7 +367,7 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); @@ -410,7 +413,7 @@ export const dynamicSecretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); @@ -452,7 +455,7 @@ export const dynamicSecretServiceFactory = ({ // verify user has access to each env in request environmentSlugs.forEach((environmentSlug) => ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ) ); diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 92cc723fe..050a02f6b 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -23,6 +23,14 @@ export enum ProjectPermissionCmekActions { Decrypt = "decrypt" } +export enum ProjectPermissionDynamicSecretActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + Lease = "lease" +} + export enum ProjectPermissionSub { Role = "role", Member = "member", @@ -88,7 +96,7 @@ export type ProjectPermissionSet = ) ] | [ - ProjectPermissionActions, + ProjectPermissionDynamicSecretActions, ( | ProjectPermissionSub.DynamicSecrets | (ForcedSubject & DynamicSecretSubjectFields) @@ -397,7 +405,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ z.object({ subject: z.literal(ProjectPermissionSub.DynamicSecrets).describe("The entity this permission pertains to."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionDynamicSecretActions).describe( "Describe what action an entity can take." ), conditions: SecretConditionV1Schema.describe( @@ -414,7 +422,6 @@ const buildAdminPermissionRules = () => { [ ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretFolders, - ProjectPermissionSub.DynamicSecrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretApproval, ProjectPermissionSub.SecretRotation, @@ -447,6 +454,17 @@ const buildAdminPermissionRules = () => { ); }); + can( + [ + ProjectPermissionDynamicSecretActions.Read, + ProjectPermissionDynamicSecretActions.Edit, + ProjectPermissionDynamicSecretActions.Create, + ProjectPermissionDynamicSecretActions.Delete, + ProjectPermissionDynamicSecretActions.Lease + ], + ProjectPermissionSub.DynamicSecrets + ); + can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project); can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback); can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms); @@ -489,10 +507,11 @@ const buildMemberPermissionRules = () => { ); can( [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete + ProjectPermissionDynamicSecretActions.Read, + ProjectPermissionDynamicSecretActions.Edit, + ProjectPermissionDynamicSecretActions.Create, + ProjectPermissionDynamicSecretActions.Delete, + ProjectPermissionDynamicSecretActions.Lease ], ProjectPermissionSub.DynamicSecrets ); @@ -629,7 +648,7 @@ const buildViewerPermissionRules = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); - can(ProjectPermissionActions.Read, ProjectPermissionSub.DynamicSecrets); + can(ProjectPermissionDynamicSecretActions.Read, ProjectPermissionSub.DynamicSecrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 2fda45943..743716392 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -3,7 +3,10 @@ import { z } from "zod"; import { SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionDynamicSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { DASHBOARD } from "@app/lib/api-docs"; import { BadRequestError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; @@ -195,7 +198,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { const allowedDynamicSecretEnviroments = // filter envs user has access to environments.filter((environment) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath }) ) ); diff --git a/frontend/src/context/ProjectPermissionContext/index.tsx b/frontend/src/context/ProjectPermissionContext/index.tsx index afad2cd86..4b04f5cb8 100644 --- a/frontend/src/context/ProjectPermissionContext/index.tsx +++ b/frontend/src/context/ProjectPermissionContext/index.tsx @@ -3,5 +3,6 @@ export type { ProjectPermissionSet, TProjectPermission } from "./types"; export { ProjectPermissionActions, ProjectPermissionCmekActions, + ProjectPermissionDynamicSecretActions, ProjectPermissionSub } from "./types"; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index c52745f24..7bb172012 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -7,6 +7,14 @@ export enum ProjectPermissionActions { Delete = "delete" } +export enum ProjectPermissionDynamicSecretActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + Lease = "lease" +} + export enum ProjectPermissionCmekActions { Read = "read", Create = "create", @@ -108,7 +116,7 @@ export type ProjectPermissionSet = ) ] | [ - ProjectPermissionActions, + ProjectPermissionDynamicSecretActions, ( | ProjectPermissionSub.DynamicSecrets | (ForcedSubject & DynamicSecretSubjectFields) diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx index ada436833..91dae5d2d 100644 --- a/frontend/src/context/index.tsx +++ b/frontend/src/context/index.tsx @@ -11,6 +11,7 @@ export type { TProjectPermission } from "./ProjectPermissionContext"; export { ProjectPermissionActions, ProjectPermissionCmekActions, + ProjectPermissionDynamicSecretActions, ProjectPermissionProvider, ProjectPermissionSub, useProjectPermission diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts index fc620cacc..7c145fd08 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts @@ -7,6 +7,7 @@ import { } from "@app/context"; import { PermissionConditionOperators, + ProjectPermissionDynamicSecretActions, TPermissionCondition, TPermissionConditionOperators } from "@app/context/ProjectPermissionContext/types"; @@ -28,6 +29,14 @@ const CmekPolicyActionSchema = z.object({ decrypt: z.boolean().optional() }); +const DynamicSecretPolicyActionSchema = z.object({ + read: z.boolean().optional(), + edit: z.boolean().optional(), + delete: z.boolean().optional(), + create: z.boolean().optional(), + lease: z.boolean().optional() +}); + const SecretRollbackPolicyActionSchema = z.object({ read: z.boolean().optional(), create: z.boolean().optional() @@ -90,7 +99,7 @@ export const formSchema = z.object({ }) .array() .default([]), - [ProjectPermissionSub.DynamicSecrets]: GeneralPolicyActionSchema.extend({ + [ProjectPermissionSub.DynamicSecrets]: DynamicSecretPolicyActionSchema.extend({ inverted: z.boolean().optional(), conditions: ConditionSchema }) @@ -174,9 +183,9 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { if ( [ ProjectPermissionSub.Secrets, + ProjectPermissionSub.DynamicSecrets, ProjectPermissionSub.SecretFolders, ProjectPermissionSub.SecretImports, - ProjectPermissionSub.DynamicSecrets, ProjectPermissionSub.Member, ProjectPermissionSub.Groups, ProjectPermissionSub.Identity, @@ -199,25 +208,50 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { ProjectPermissionSub.Kms ].includes(subject) ) { - const canRead = action.includes(ProjectPermissionActions.Read); - const canEdit = action.includes(ProjectPermissionActions.Edit); - const canDelete = action.includes(ProjectPermissionActions.Delete); - const canCreate = action.includes(ProjectPermissionActions.Create); - // from above statement we are sure it won't be undefined if (isConditionalSubjects(subject)) { if (!formVal[subject]) formVal[subject] = []; - formVal[subject]!.push({ - read: canRead, - create: canCreate, - edit: canEdit, - delete: canDelete, - conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], - inverted - }); + + if (subject === ProjectPermissionSub.DynamicSecrets) { + const canRead = action.includes(ProjectPermissionDynamicSecretActions.Read); + const canEdit = action.includes(ProjectPermissionDynamicSecretActions.Edit); + const canDelete = action.includes(ProjectPermissionDynamicSecretActions.Delete); + const canCreate = action.includes(ProjectPermissionDynamicSecretActions.Create); + const canLease = action.includes(ProjectPermissionDynamicSecretActions.Lease); + + // from above statement we are sure it won't be undefined + formVal[subject]!.push({ + read: canRead, + create: canCreate, + edit: canEdit, + delete: canDelete, + conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], + inverted, + lease: canLease + }); + } else { + // for other subjects + const canRead = action.includes(ProjectPermissionActions.Read); + const canEdit = action.includes(ProjectPermissionActions.Edit); + const canDelete = action.includes(ProjectPermissionActions.Delete); + const canCreate = action.includes(ProjectPermissionActions.Create); + formVal[subject]!.push({ + read: canRead, + create: canCreate, + edit: canEdit, + delete: canDelete, + conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], + inverted + }); + } } else { // deduplicate multiple rules for other policies // because they don't have condition it doesn't make sense for multiple rules + const canRead = action.includes(ProjectPermissionActions.Read); + const canEdit = action.includes(ProjectPermissionActions.Edit); + const canDelete = action.includes(ProjectPermissionActions.Delete); + const canCreate = action.includes(ProjectPermissionActions.Create); + if (!formVal[subject]) formVal[subject] = [{}]; if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true; if (canEdit) formVal[subject as ProjectPermissionSub.Member]![0].edit = true; @@ -317,7 +351,7 @@ export type TProjectPermissionObject = { label: string; value: keyof Omit< NonNullable[K]>[number], - "conditions" + "conditions" | "inverted" >; }[]; }; @@ -357,7 +391,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { { label: "Read", value: "read" }, { label: "Create", value: "create" }, { label: "Modify", value: "edit" }, - { label: "Remove", value: "delete" } + { label: "Remove", value: "delete" }, + { label: "Lease", value: "lease" } ] }, [ProjectPermissionSub.Cmek]: { diff --git a/frontend/src/views/SecretMainPage/SecretMainPage.tsx b/frontend/src/views/SecretMainPage/SecretMainPage.tsx index ea348f083..92a1b257a 100644 --- a/frontend/src/views/SecretMainPage/SecretMainPage.tsx +++ b/frontend/src/views/SecretMainPage/SecretMainPage.tsx @@ -12,6 +12,7 @@ import { PermissionDeniedBanner } from "@app/components/permissions"; import { ContentLoader, Pagination } from "@app/components/v2"; import { ProjectPermissionActions, + ProjectPermissionDynamicSecretActions, ProjectPermissionSub, useProjectPermission, useWorkspace @@ -93,7 +94,7 @@ export const SecretMainPage = () => { ); const canReadDynamicSecret = permission.can( - ProjectPermissionActions.Read, + ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath }) ); diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx index 8da0b9167..3c1424ada 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx @@ -45,7 +45,12 @@ import { Tooltip, UpgradePlanModal } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionDynamicSecretActions, + ProjectPermissionSub, + useSubscription +} from "@app/context"; import { usePopUp } from "@app/hooks"; import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api"; import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries"; @@ -482,8 +487,8 @@ export const ActionBar = ({ )}
{status === DynamicSecretLeaseStatus.FailedDeletion && ( {!isLeaseLoading && Boolean(leases?.length) && (
- + + {(isAllowed) => ( + + )} + diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx index 001890b8f..160991f2a 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx @@ -18,7 +18,7 @@ import { Tag, Tooltip } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { ProjectPermissionDynamicSecretActions, ProjectPermissionSub } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useDeleteDynamicSecret } from "@app/hooks/api"; import { @@ -132,17 +132,27 @@ export const DynamicSecretListView = ({ )}
- + {(isAllowed) => ( + + )} + + {secret.status === DynamicSecretStatus.FailedDeletion && (