From 97567d06d4b0ae0e0e2be036f49c3a8510beb609 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 5 Mar 2025 23:47:40 +0400 Subject: [PATCH 01/15] Revert "Revert "feat(api/secrets): view secret value permission"" --- .../e2e-test/routes/v2/service-token.spec.ts | 10 +- backend/e2e-test/vitest-environment-knex.ts | 1 - ...kfill-secret-permissions-with-readvalue.ts | 313 +++++++++ .../v1/secret-approval-request-router.ts | 21 +- backend/src/ee/routes/v1/secret-router.ts | 4 +- .../src/ee/routes/v1/secret-version-router.ts | 7 +- backend/src/ee/routes/v1/snapshot-router.ts | 12 +- .../services/permission/project-permission.ts | 60 +- .../secret-approval-request-service.ts | 20 +- .../secret-replication-service.ts | 1 + .../secret-rotation-service.ts | 8 +- .../secret-snapshot-service.ts | 144 ++-- backend/src/lib/api-docs/constants.ts | 3 + backend/src/lib/errors/index.ts | 1 + backend/src/server/routes/sanitizedSchemas.ts | 9 + .../src/server/routes/v1/dashboard-router.ts | 90 +-- .../server/routes/v2/service-token-router.ts | 2 +- backend/src/server/routes/v3/secret-router.ts | 148 ++-- .../external-migration-fns.ts | 4 +- .../external-migration-queue.ts | 4 +- .../integration-delete-secret.ts | 3 +- .../integration/integration-service.ts | 10 +- .../src/services/project/project-service.ts | 11 +- .../secret-import/secret-import-fns.ts | 29 +- .../secret-import/secret-import-service.ts | 22 +- .../services/secret-sync/secret-sync-queue.ts | 3 +- .../secret-sync/secret-sync-service.ts | 6 +- .../src/services/secret-tag/secret-tag-dal.ts | 1 + .../secret-v2-bridge/secret-v2-bridge-fns.ts | 44 +- .../secret-v2-bridge-service.ts | 654 +++++++++++++----- .../secret-v2-bridge-types.ts | 18 +- .../secret-v2-bridge/secret-version-dal.ts | 59 +- backend/src/services/secret/secret-dal.ts | 45 +- backend/src/services/secret/secret-fns.ts | 42 +- backend/src/services/secret/secret-queue.ts | 3 +- backend/src/services/secret/secret-service.ts | 276 ++++++-- backend/src/services/secret/secret-types.ts | 7 +- .../src/services/secret/secret-version-dal.ts | 49 +- .../service-token/service-token-service.ts | 8 +- .../service-token/service-token-types.ts | 2 +- .../SecretReferenceDetails.tsx | 45 +- frontend/src/components/v2/Blur/Blur.tsx | 22 + frontend/src/components/v2/Blur/index.tsx | 1 + .../InfisicalSecretInput.tsx | 1 + .../context/ProjectPermissionContext/types.ts | 10 +- frontend/src/hooks/api/dashboard/queries.tsx | 3 + frontend/src/hooks/api/dashboard/types.ts | 1 + .../api/secretApprovalRequest/queries.tsx | 1 + .../src/hooks/api/secretImports/queries.tsx | 2 + .../src/hooks/api/secretSnapshots/queries.tsx | 1 + frontend/src/hooks/api/secrets/queries.tsx | 24 +- frontend/src/hooks/api/secrets/types.ts | 5 + frontend/src/hooks/api/types.ts | 9 +- .../AddServiceTokenModal.tsx | 8 +- .../components/GeneralPermissionPolicies.tsx | 27 +- .../ProjectRoleModifySection.utils.tsx | 36 +- .../CreateSecretForm/CreateSecretForm.tsx | 3 +- .../SecretOverviewTableRow/SecretEditRow.tsx | 43 +- .../SecretNoAccessOverviewTableRow.tsx | 3 +- .../SecretOverviewTableRow.tsx | 9 +- .../SecretRenameRow.tsx | 12 +- .../components/QuickSearchSecretItem.tsx | 62 +- .../SelectionPanel/SelectionPanel.tsx | 5 +- .../MoveSecretsDialog/MoveSecretsDialog.tsx | 5 +- .../SecretDashboardPage.tsx | 13 +- .../components/ActionBar/ActionBar.tsx | 103 +-- .../SecretListView/SecretDetailSidebar.tsx | 174 +++-- .../components/SecretListView/SecretItem.tsx | 24 +- .../SecretListView/SecretNoAccessListView.tsx | 9 +- .../components/SnapshotView/SecretItem.tsx | 19 +- 70 files changed, 2105 insertions(+), 729 deletions(-) create mode 100644 backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts create mode 100644 frontend/src/components/v2/Blur/Blur.tsx create mode 100644 frontend/src/components/v2/Blur/index.tsx diff --git a/backend/e2e-test/routes/v2/service-token.spec.ts b/backend/e2e-test/routes/v2/service-token.spec.ts index 6cc8f6e34..bfe043523 100644 --- a/backend/e2e-test/routes/v2/service-token.spec.ts +++ b/backend/e2e-test/routes/v2/service-token.spec.ts @@ -6,7 +6,7 @@ import { decryptAsymmetric, decryptSymmetric128BitHexKeyUTF8, encryptSymmetric12 const createServiceToken = async ( scopes: { environment: string; secretPath: string }[], - permissions: ("read" | "write")[] + permissions: ("read" | "write" | "readValue")[] ) => { const projectKeyRes = await testServer.inject({ method: "GET", @@ -139,7 +139,7 @@ describe("Service token secret ops", async () => { beforeAll(async () => { serviceToken = await createServiceToken( [{ secretPath: "/**", environment: seedData1.environment.slug }], - ["read", "write"] + ["read", "write", "readValue"] ); // this is ensure cli service token decryptiong working fine @@ -496,7 +496,7 @@ describe("Service token fail cases", async () => { test("Unauthorized secret path access", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read", "write"] + ["read", "readValue", "write"] ); const fetchSecrets = await testServer.inject({ method: "GET", @@ -518,7 +518,7 @@ describe("Service token fail cases", async () => { test("Unauthorized secret environment access", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read", "write"] + ["read", "readValue", "write"] ); const fetchSecrets = await testServer.inject({ method: "GET", @@ -540,7 +540,7 @@ describe("Service token fail cases", async () => { test("Unauthorized write operation", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read"] + ["read", "readValue"] ); const writeSecrets = await testServer.inject({ method: "POST", diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index 9dfb38aeb..46b322349 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -120,4 +120,3 @@ export default { }; } }; - diff --git a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts new file mode 100644 index 000000000..9e567cefe --- /dev/null +++ b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts @@ -0,0 +1,313 @@ +import { MongoAbility, RawRuleOf } from "@casl/ability"; +import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; +import { Knex } from "knex"; +import { z } from "zod"; + +import { selectAllTableCols } from "@app/lib/knex"; + +import { TableName } from "../schemas"; + +enum ProjectPermissionSub { + Secrets = "secrets" +} + +enum SecretActions { + Read = "read", + ReadValue = "readValue" +} + +const UnpackedPermissionSchema = z.object({ + subject: z + .union([z.string().min(1), z.string().array()]) + .transform((el) => (typeof el !== "string" ? el[0] : el)) + .optional(), + action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)), + conditions: z.unknown().optional(), + inverted: z.boolean().optional() +}); + +const $unpackPermissions = (permissions: unknown) => + UnpackedPermissionSchema.array().parse(unpackRules((permissions || []) as PackRule>[])); + +const $updatePermissionsUp = (permissions: unknown) => { + const parsedPermissions = $unpackPermissions(permissions); + let shouldUpdate = false; + + for (let i = 0; i < parsedPermissions.length; i += 1) { + const parsedPermission = parsedPermissions[i]; + const { subject, action } = parsedPermission; + + if (subject === ProjectPermissionSub.Secrets) { + if (action.includes(SecretActions.Read) && !action.includes(SecretActions.ReadValue)) { + action.push(SecretActions.ReadValue); + parsedPermissions[i] = { ...parsedPermission, action }; + shouldUpdate = true; + } + } + } + + return { + parsedPermissions, + shouldUpdate + }; +}; + +const $updatePermissionsDown = (permissions: unknown) => { + const parsedPermissions = $unpackPermissions(permissions); + + let shouldUpdate = false; + for (let i = 0; i < parsedPermissions.length; i += 1) { + const parsedPermission = parsedPermissions[i]; + + const { subject, action } = parsedPermission; + + if (subject === ProjectPermissionSub.Secrets) { + const readValueIndex = action.indexOf(SecretActions.ReadValue); + + if (action.includes(SecretActions.ReadValue) && readValueIndex !== -1) { + action.splice(readValueIndex, 1); + parsedPermissions[i] = { ...parsedPermission, action }; + + shouldUpdate = true; + } + } + } + + const repackedPermissions = packRules(parsedPermissions); + + return { + repackedPermissions, + shouldUpdate + }; +}; + +const CHUNK_SIZE = 1000; + +export async function up(knex: Knex): Promise { + const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles)); + const projectIdentityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select( + selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege) + ); + const projectUserAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select( + selectAllTableCols(TableName.ProjectUserAdditionalPrivilege) + ); + + const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken)); + + const updatedServiceTokens = serviceTokens.reduce((acc, serviceToken) => { + const { permissions } = serviceToken; // Service tokens are special, and include an array of actions only. + + if (permissions.includes(SecretActions.Read) && !permissions.includes(SecretActions.ReadValue)) { + permissions.push(SecretActions.ReadValue); + acc.push({ + ...serviceToken, + permissions + }); + } + return acc; + }, []); + + if (updatedServiceTokens.length > 0) { + for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) { + const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ServiceToken) + .whereIn( + "id", + chunk.map((t) => t.id) + ) + .update({ + // @ts-expect-error -- raw query + permissions: knex.raw( + `CASE id + ${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")} + END`, + chunk.map((t) => t.permissions) + ) + }); + } + } + + const updatedRoles = projectRoles.reduce((acc, projectRole) => { + const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(projectRole.permissions); + + if (shouldUpdate) { + acc.push({ + ...projectRole, + permissions: JSON.stringify(packRules(parsedPermissions)) + }); + } + return acc; + }, []); + + const updatedIdentityAdditionalPrivileges = projectIdentityAdditionalPrivileges.reduce< + typeof projectIdentityAdditionalPrivileges + >((acc, identityAdditionalPrivilege) => { + const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(identityAdditionalPrivilege.permissions); + + if (shouldUpdate) { + acc.push({ + ...identityAdditionalPrivilege, + permissions: JSON.stringify(packRules(parsedPermissions)) + }); + } + return acc; + }, []); + + const updatedUserAdditionalPrivileges = projectUserAdditionalPrivileges.reduce< + typeof projectUserAdditionalPrivileges + >((acc, userAdditionalPrivilege) => { + const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(userAdditionalPrivilege.permissions); + + if (shouldUpdate) { + acc.push({ + ...userAdditionalPrivilege, + permissions: JSON.stringify(packRules(parsedPermissions)) + }); + } + return acc; + }, []); + + if (updatedRoles.length > 0) { + for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) { + const chunk = updatedRoles.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]); + } + } + + if (updatedIdentityAdditionalPrivileges.length > 0) { + for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) { + const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); + } + } + + if (updatedUserAdditionalPrivileges.length > 0) { + for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) { + const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); + } + } +} + +export async function down(knex: Knex): Promise { + const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles)); + const identityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select( + selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege) + ); + const userAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select( + selectAllTableCols(TableName.ProjectUserAdditionalPrivilege) + ); + const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken)); + + const updatedServiceTokens = serviceTokens.reduce((acc, serviceToken) => { + const { permissions } = serviceToken; + + if (permissions.includes(SecretActions.ReadValue)) { + permissions.splice(permissions.indexOf(SecretActions.ReadValue), 1); + acc.push({ + ...serviceToken, + permissions + }); + } + return acc; + }, []); + + if (updatedServiceTokens.length > 0) { + for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) { + const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ServiceToken) + .whereIn( + "id", + chunk.map((t) => t.id) + ) + .update({ + // @ts-expect-error -- raw query + permissions: knex.raw( + `CASE id + ${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")} + END`, + chunk.map((t) => t.permissions) + ) + }); + } + } + + const updatedRoles = projectRoles.reduce((acc, projectRole) => { + const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(projectRole.permissions); + + if (shouldUpdate) { + acc.push({ + ...projectRole, + permissions: JSON.stringify(repackedPermissions) + }); + } + return acc; + }, []); + + const updatedIdentityAdditionalPrivileges = identityAdditionalPrivileges.reduce( + (acc, identityAdditionalPrivilege) => { + const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(identityAdditionalPrivilege.permissions); + + if (shouldUpdate) { + acc.push({ + ...identityAdditionalPrivilege, + permissions: JSON.stringify(repackedPermissions) + }); + } + return acc; + }, + [] + ); + + const updatedUserAdditionalPrivileges = userAdditionalPrivileges.reduce( + (acc, userAdditionalPrivilege) => { + const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(userAdditionalPrivilege.permissions); + + if (shouldUpdate) { + acc.push({ + ...userAdditionalPrivilege, + permissions: JSON.stringify(repackedPermissions) + }); + } + return acc; + }, + [] + ); + + if (updatedRoles.length > 0) { + for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) { + const chunk = updatedRoles.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]); + } + } + + if (updatedIdentityAdditionalPrivileges.length > 0) { + for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) { + const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); + } + } + + if (updatedUserAdditionalPrivileges.length > 0) { + for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) { + const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE); + + // eslint-disable-next-line no-await-in-loop + await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); + } + } +} diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index 653d04d4f..1ceb6019b 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -1,16 +1,11 @@ import { z } from "zod"; -import { - SecretApprovalRequestsReviewersSchema, - SecretApprovalRequestsSchema, - SecretTagsSchema, - UsersSchema -} from "@app/db/schemas"; +import { SecretApprovalRequestsReviewersSchema, SecretApprovalRequestsSchema, UsersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; +import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; @@ -250,14 +245,6 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv } }); - const tagSchema = SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .array() - .optional(); - server.route({ method: "GET", url: "/:id", @@ -291,7 +278,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv .omit({ _id: true, environment: true, workspace: true, type: true, version: true }) .extend({ op: z.string(), - tags: tagSchema, + tags: SanitizedTagSchema.array().optional(), secretMetadata: ResourceMetadataSchema.nullish(), secret: z .object({ @@ -310,7 +297,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv secretKey: z.string(), secretValue: z.string().optional(), secretComment: z.string().optional(), - tags: tagSchema, + tags: SanitizedTagSchema.array().optional(), secretMetadata: ResourceMetadataSchema.nullish() }) .optional() diff --git a/backend/src/ee/routes/v1/secret-router.ts b/backend/src/ee/routes/v1/secret-router.ts index 4c249afe0..a964eb1b8 100644 --- a/backend/src/ee/routes/v1/secret-router.ts +++ b/backend/src/ee/routes/v1/secret-router.ts @@ -1,6 +1,6 @@ import z from "zod"; -import { ProjectPermissionActions } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { RAW_SECRETS } from "@app/lib/api-docs"; import { removeTrailingSlash } from "@app/lib/fn"; import { readLimit } from "@app/server/config/rateLimiter"; @@ -9,7 +9,7 @@ import { AuthMode } from "@app/services/auth/auth-type"; const AccessListEntrySchema = z .object({ - allowedActions: z.nativeEnum(ProjectPermissionActions).array(), + allowedActions: z.nativeEnum(ProjectPermissionSecretActions).array(), id: z.string(), membershipId: z.string(), name: z.string() diff --git a/backend/src/ee/routes/v1/secret-version-router.ts b/backend/src/ee/routes/v1/secret-version-router.ts index 11443ebfe..a09a05c91 100644 --- a/backend/src/ee/routes/v1/secret-version-router.ts +++ b/backend/src/ee/routes/v1/secret-version-router.ts @@ -22,7 +22,11 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) => }), response: { 200: z.object({ - secretVersions: secretRawSchema.array() + secretVersions: secretRawSchema + .extend({ + secretValueHidden: z.boolean() + }) + .array() }) } }, @@ -37,6 +41,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) => offset: req.query.offset, secretId: req.params.secretId }); + return { secretVersions }; } }); diff --git a/backend/src/ee/routes/v1/snapshot-router.ts b/backend/src/ee/routes/v1/snapshot-router.ts index fc2d25712..283b9b31e 100644 --- a/backend/src/ee/routes/v1/snapshot-router.ts +++ b/backend/src/ee/routes/v1/snapshot-router.ts @@ -1,10 +1,10 @@ import { z } from "zod"; -import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas"; +import { SecretSnapshotsSchema } from "@app/db/schemas"; import { PROJECTS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; +import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerSnapshotRouter = async (server: FastifyZodProvider) => { @@ -31,12 +31,9 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { secretVersions: secretRawSchema .omit({ _id: true, environment: true, workspace: true, type: true }) .extend({ + secretValueHidden: z.boolean(), secretId: z.string(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }).array() + tags: SanitizedTagSchema.array() }) .array(), folderVersion: z.object({ id: z.string(), name: z.string() }).array(), @@ -55,6 +52,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, id: req.params.secretSnapshotId }); + return { secretSnapshot }; } }); diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 4389c3866..3f69c481a 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -17,6 +17,14 @@ export enum ProjectPermissionActions { Delete = "delete" } +export enum ProjectPermissionSecretActions { + DescribeSecret = "read", + ReadValue = "readValue", + Create = "create", + Edit = "edit", + Delete = "delete" +} + export enum ProjectPermissionCmekActions { Read = "read", Create = "create", @@ -115,7 +123,7 @@ export type IdentityManagementSubjectFields = { export type ProjectPermissionSet = | [ - ProjectPermissionActions, + ProjectPermissionSecretActions, ProjectPermissionSub.Secrets | (ForcedSubject & SecretSubjectFields) ] | [ @@ -429,6 +437,7 @@ const GeneralPermissionSchema = [ }) ]; +// Do not update this schema anymore, as it's kept purely for backwards compatability. Update V2 schema only. export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ z.object({ subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), @@ -460,7 +469,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ z.object({ subject: z.literal(ProjectPermissionSub.Secrets).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(ProjectPermissionSecretActions).describe( "Describe what action an entity can take." ), conditions: SecretConditionV2Schema.describe( @@ -517,7 +526,6 @@ const buildAdminPermissionRules = () => { // Admins get full access to everything [ - ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretFolders, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretApproval, @@ -550,10 +558,21 @@ const buildAdminPermissionRules = () => { ProjectPermissionActions.Create, ProjectPermissionActions.Delete ], - el as ProjectPermissionSub + el ); }); + can( + [ + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.Delete + ], + ProjectPermissionSub.Secrets + ); + can( [ ProjectPermissionDynamicSecretActions.ReadRootCredential, @@ -613,10 +632,11 @@ const buildMemberPermissionRules = () => { can( [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Delete ], ProjectPermissionSub.Secrets ); @@ -788,7 +808,8 @@ export const projectMemberPermissions = buildMemberPermissionRules(); const buildViewerPermissionRules = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); + can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); @@ -831,6 +852,8 @@ export const buildServiceTokenProjectPermission = ( ) => { const canWrite = permission.includes("write"); const canRead = permission.includes("read"); + const canReadValue = permission.includes("readValue"); + const { can, build } = new AbilityBuilder>(createMongoAbility); scopes.forEach(({ secretPath, environment }) => { [ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretFolders].forEach( @@ -860,6 +883,14 @@ export const buildServiceTokenProjectPermission = ( environment }); } + + if (subject === ProjectPermissionSub.Secrets && canReadValue) { + // @ts-expect-error type + can(ProjectPermissionSecretActions.ReadValue, subject as ProjectPermissionSub.Secrets, { + secretPath: { $glob: secretPath }, + environment + }); + } } ); }); @@ -916,7 +947,17 @@ export const backfillPermissionV1SchemaToV2Schema = ( subject: ProjectPermissionSub.SecretImports as const })); + const secretPolicies = secretSubjects.map(({ subject, ...el }) => ({ + subject: ProjectPermissionSub.Secrets as const, + ...el, + action: + el.action.includes(ProjectPermissionActions.Read) && !el.action.includes(ProjectPermissionSecretActions.ReadValue) + ? el.action.concat(ProjectPermissionSecretActions.ReadValue) + : el.action + })); + const secretFolderPolicies = secretSubjects + .map(({ subject, ...el }) => ({ ...el, // read permission is not needed anymore @@ -958,6 +999,7 @@ export const backfillPermissionV1SchemaToV2Schema = ( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error this is valid ts secretImportPolicies, + secretPolicies, dynamicSecretPolicies, hasReadOnlyFolder.length ? [] : secretFolderPolicies ); diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index d296d7975..8efae75e6 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -58,7 +58,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal"; import { TLicenseServiceFactory } from "../license/license-service"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission"; import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal"; import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service"; import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal"; @@ -88,7 +88,12 @@ type TSecretApprovalRequestServiceFactoryDep = { secretDAL: TSecretDALFactory; secretTagDAL: Pick< TSecretTagDALFactory, - "findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "saveTagsToSecretV2" | "deleteTagsToSecretV2" + | "findManyTagsById" + | "saveTagsToSecret" + | "deleteTagsManySecret" + | "saveTagsToSecretV2" + | "deleteTagsToSecretV2" + | "find" >; secretBlindIndexDAL: Pick; snapshotService: Pick; @@ -106,7 +111,7 @@ type TSecretApprovalRequestServiceFactoryDep = { kmsService: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" + "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; @@ -913,7 +918,7 @@ export const secretApprovalRequestServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -1000,6 +1005,7 @@ export const secretApprovalRequestServiceFactory = ({ : keyName2BlindIndex[secretName]; // add tags if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds; + return { ...latestSecretVersions[secretId], ...el, @@ -1363,9 +1369,9 @@ export const secretApprovalRequestServiceFactory = ({ const tagsGroupById = groupBy(tags, (i) => i.id); commits.forEach((commit) => { - let action = ProjectPermissionActions.Create; - if (commit.op === SecretOperations.Update) action = ProjectPermissionActions.Edit; - if (commit.op === SecretOperations.Delete) action = ProjectPermissionActions.Delete; + let action = ProjectPermissionSecretActions.Create; + if (commit.op === SecretOperations.Update) action = ProjectPermissionSecretActions.Edit; + if (commit.op === SecretOperations.Delete) action = ProjectPermissionSecretActions.Delete; ForbiddenError.from(permission).throwUnlessCan( action, diff --git a/backend/src/ee/services/secret-replication/secret-replication-service.ts b/backend/src/ee/services/secret-replication/secret-replication-service.ts index 3c25db98c..5fae2675d 100644 --- a/backend/src/ee/services/secret-replication/secret-replication-service.ts +++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts @@ -265,6 +265,7 @@ export const secretReplicationServiceFactory = ({ folderDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), + viewSecretValue: true, hasSecretAccess: () => true }); // secrets that gets replicated across imports diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts index 02da4b7ea..8d458111a 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts @@ -15,7 +15,11 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret import { TLicenseServiceFactory } from "../license/license-service"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "../permission/project-permission"; import { TSecretRotationDALFactory } from "./secret-rotation-dal"; import { TSecretRotationQueueFactory } from "./secret-rotation-queue"; import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types"; @@ -106,7 +110,7 @@ export const secretRotationServiceFactory = ({ }); } ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index dc2cff456..397824904 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -23,7 +23,11 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se import { TLicenseServiceFactory } from "../license/license-service"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "../permission/project-permission"; import { TGetSnapshotDataDTO, TProjectSnapshotCountDTO, @@ -35,6 +39,7 @@ import { TSnapshotFolderDALFactory } from "./snapshot-folder-dal"; import { TSnapshotSecretDALFactory } from "./snapshot-secret-dal"; import { TSnapshotSecretV2DALFactory } from "./snapshot-secret-v2-dal"; import { getFullFolderPath } from "./snapshot-service-fns"; +import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns"; type TSecretSnapshotServiceFactoryDep = { snapshotDAL: TSnapshotDALFactory; @@ -98,7 +103,7 @@ export const secretSnapshotServiceFactory = ({ // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -135,7 +140,7 @@ export const secretSnapshotServiceFactory = ({ // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -162,6 +167,7 @@ export const secretSnapshotServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); + const shouldUseBridge = snapshot.projectVersion === 3; let snapshotDetails; if (shouldUseBridge) { @@ -170,68 +176,110 @@ export const secretSnapshotServiceFactory = ({ projectId: snapshot.projectId }); const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id); + + const fullFolderPath = await getFullFolderPath({ + folderDAL, + folderId: encryptedSnapshotDetails.folderId, + envId: encryptedSnapshotDetails.environment.id + }); + snapshotDetails = { ...encryptedSnapshotDetails, - secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({ - ...el, - secretKey: el.key, - secretValue: el.encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() - : "", - secretComment: el.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() - : "" - })) + secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => { + const canReadValue = permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: encryptedSnapshotDetails.environment.slug, + secretPath: fullFolderPath, + secretName: el.key, + secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined + }) + ); + + let secretValue = ""; + if (canReadValue) { + secretValue = el.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() + : ""; + } else { + secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK; + } + + return { + ...el, + secretKey: el.key, + secretValueHidden: !canReadValue, + secretValue, + secretComment: el.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() + : "" + }; + }) }; } else { const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id); + + const fullFolderPath = await getFullFolderPath({ + folderDAL, + folderId: encryptedSnapshotDetails.folderId, + envId: encryptedSnapshotDetails.environment.id + }); + const { botKey } = await projectBotService.getBotKey(snapshot.projectId); if (!botKey) throw new NotFoundError({ message: `Project bot key not found for project with ID '${snapshot.projectId}'` }); snapshotDetails = { ...encryptedSnapshotDetails, - secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({ - ...el, - secretKey: decryptSymmetric128BitHexKeyUTF8({ + secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: el.secretKeyCiphertext, iv: el.secretKeyIV, tag: el.secretKeyTag, key: botKey - }), - secretValue: decryptSymmetric128BitHexKeyUTF8({ - ciphertext: el.secretValueCiphertext, - iv: el.secretValueIV, - tag: el.secretValueTag, - key: botKey - }), - secretComment: - el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext - ? decryptSymmetric128BitHexKeyUTF8({ - ciphertext: el.secretCommentCiphertext, - iv: el.secretCommentIV, - tag: el.secretCommentTag, - key: botKey - }) - : "" - })) + }); + + const canReadValue = permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: encryptedSnapshotDetails.environment.slug, + secretPath: fullFolderPath, + secretName: secretKey, + secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined + }) + ); + + let secretValue = ""; + + if (canReadValue) { + secretValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: el.secretValueCiphertext, + iv: el.secretValueIV, + tag: el.secretValueTag, + key: botKey + }); + } else { + secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK; + } + + return { + ...el, + secretKey, + secretValueHidden: !canReadValue, + secretValue, + secretComment: + el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext + ? decryptSymmetric128BitHexKeyUTF8({ + ciphertext: el.secretCommentCiphertext, + iv: el.secretCommentIV, + tag: el.secretCommentTag, + key: botKey + }) + : "" + }; + }) }; } - const fullFolderPath = await getFullFolderPath({ - folderDAL, - folderId: snapshotDetails.folderId, - envId: snapshotDetails.environment.id - }); - - // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: snapshotDetails.environment.slug, - secretPath: fullFolderPath - }) - ); - return snapshotDetails; }; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 94076cf25..fe893d5ab 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -667,6 +667,7 @@ export const SECRETS = { secretPath: "The path of the secret to attach tags to.", type: "The type of the secret to attach tags to. (shared/personal)", environment: "The slug of the environment where the secret is located", + viewSecretValue: "Whether or not to retrieve the secret value.", projectSlug: "The slug of the project where the secret is located.", tagSlugs: "An array of existing tag slugs to attach to the secret." }, @@ -690,6 +691,7 @@ export const RAW_SECRETS = { "The slug of the project to list secrets from. This parameter is only applicable by machine identities.", environment: "The slug of the environment to list secrets from.", secretPath: "The secret path to list secrets from.", + viewSecretValue: "Whether or not to retrieve the secret value.", includeImports: "Weather to include imported secrets or not.", tagSlugs: "The comma separated tag slugs to filter secrets.", metadataFilter: @@ -718,6 +720,7 @@ export const RAW_SECRETS = { secretPath: "The path of the secret to get.", version: "The version of the secret to get.", type: "The type of the secret to get.", + viewSecretValue: "Whether or not to retrieve the secret value.", includeImports: "Weather to include imported secrets or not." }, UPDATE: { diff --git a/backend/src/lib/errors/index.ts b/backend/src/lib/errors/index.ts index f3dc83f26..950bfd29f 100644 --- a/backend/src/lib/errors/index.ts +++ b/backend/src/lib/errors/index.ts @@ -1,4 +1,5 @@ /* eslint-disable max-classes-per-file */ + export class DatabaseError extends Error { name: string; diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index a6cc8bae0..77149eb1b 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -7,6 +7,7 @@ import { ProjectRolesSchema, ProjectsSchema, SecretApprovalPoliciesSchema, + SecretTagsSchema, UsersSchema } from "@app/db/schemas"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -241,3 +242,11 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({ kmsCertificateKeyId: true, auditLogsRetentionDays: true }); + +export const SanitizedTagSchema = SecretTagsSchema.pick({ + id: true, + slug: true, + color: true +}).extend({ + name: z.string() +}); diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index d96c45f04..27ba3f8a3 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -1,10 +1,11 @@ import { ForbiddenError, subject } from "@casl/ability"; import { z } from "zod"; -import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas"; +import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { ProjectPermissionDynamicSecretActions, + ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { DASHBOARD } from "@app/lib/api-docs"; @@ -15,7 +16,7 @@ import { secretsLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getUserAgentType } from "@app/server/plugins/audit-log"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; +import { SanitizedDynamicSecretSchema, SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; @@ -116,16 +117,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(), secrets: secretRawSchema .extend({ + secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + tags: SanitizedTagSchema.array().optional() }) .array() .optional(), @@ -294,6 +289,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { secrets = await server.services.secret.getSecretsRawMultiEnv({ + viewSecretValue: true, actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, @@ -393,6 +389,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { .optional(), search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(), tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(), + viewSecretValue: booleanSchema.default(true), includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets), includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders), includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets), @@ -410,16 +407,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(), secrets: secretRawSchema .extend({ + secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + tags: SanitizedTagSchema.array().optional() }) .array() .optional(), @@ -601,23 +592,25 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }); if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { - const secretsRaw = await server.services.secret.getSecretsRaw({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - environment, - actorAuthMethod: req.permission.authMethod, - projectId, - path: secretPath, - orderBy, - orderDirection, - search, - limit: remainingLimit, - offset: adjustedOffset, - tagSlugs: tags - }); - - secrets = secretsRaw.secrets; + secrets = ( + await server.services.secret.getSecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + viewSecretValue: req.query.viewSecretValue, + throwOnMissingReadValuePermission: false, + actorOrgId: req.permission.orgId, + environment, + actorAuthMethod: req.permission.authMethod, + projectId, + path: secretPath, + orderBy, + orderDirection, + search, + limit: remainingLimit, + offset: adjustedOffset, + tagSlugs: tags + }) + ).secrets; await server.services.auditLog.createAuditLog({ projectId, @@ -696,16 +689,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { .optional(), secrets: secretRawSchema .extend({ + secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + tags: SanitizedTagSchema.array().optional() }) .array() .optional() @@ -749,6 +736,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { const secrets = await server.services.secret.getSecretsRawByFolderMappings( { + filterByAction: ProjectPermissionSecretActions.DescribeSecret, projectId, folderMappings, filters: { @@ -862,22 +850,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { projectId: z.string().trim(), environment: z.string().trim(), secretPath: z.string().trim().default("/").transform(removeTrailingSlash), - keys: z.string().trim().transform(decodeURIComponent) + keys: z.string().trim().transform(decodeURIComponent), + viewSecretValue: booleanSchema.default(false) }), response: { 200: z.object({ secrets: secretRawSchema .extend({ + secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + tags: SanitizedTagSchema.array().optional() }) .array() .optional() @@ -886,7 +869,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { secretPath, projectId, environment } = req.query; + const { secretPath, projectId, environment, viewSecretValue } = req.query; const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? []; if (!keys.length) throw new BadRequestError({ message: "One or more keys required" }); @@ -895,6 +878,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, + viewSecretValue, environment, actorAuthMethod: req.permission.authMethod, projectId, diff --git a/backend/src/server/routes/v2/service-token-router.ts b/backend/src/server/routes/v2/service-token-router.ts index fb10f17db..1a84c9123 100644 --- a/backend/src/server/routes/v2/service-token-router.ts +++ b/backend/src/server/routes/v2/service-token-router.ts @@ -94,7 +94,7 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) => iv: z.string().trim(), tag: z.string().trim(), expiresIn: z.number().nullable(), - permissions: z.enum(["read", "write"]).array() + permissions: z.enum(["read", "write", "readValue"]).array() }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 4935345dc..b68d042f0 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -1,13 +1,7 @@ import picomatch from "picomatch"; import { z } from "zod"; -import { - SecretApprovalRequestsSchema, - SecretsSchema, - SecretTagsSchema, - SecretType, - ServiceTokenScopes -} from "@app/db/schemas"; +import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -23,7 +17,7 @@ import { SecretOperations, SecretProtectionType } from "@app/services/secret/sec import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; -import { secretRawSchema } from "../sanitizedSchemas"; +import { SanitizedTagSchema, secretRawSchema } from "../sanitizedSchemas"; const SecretReferenceNode = z.object({ key: z.string(), @@ -31,6 +25,14 @@ const SecretReferenceNode = z.object({ environment: z.string(), secretPath: z.string() }); + +const convertStringBoolean = (defaultValue: boolean = false) => { + return z + .enum(["true", "false"]) + .default(defaultValue ? "true" : "false") + .transform((value) => value === "true"); +}; + type TSecretReferenceNode = z.infer & { children: TSecretReferenceNode[] }; const SecretReferenceNodeTree: z.ZodType = SecretReferenceNode.extend({ @@ -75,17 +77,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( - z.object({ - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - }) - ) + secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ + tags: SanitizedTagSchema.array() + }) }) } }, @@ -139,13 +133,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() + tags: SanitizedTagSchema.array() }) }) } @@ -247,21 +235,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug), environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), - expandSecretReferences: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.LIST.expand), - recursive: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.LIST.recursive), - include_imports: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.LIST.includeImports), + viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.LIST.viewSecretValue), + expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.LIST.expand), + recursive: convertStringBoolean().describe(RAW_SECRETS.LIST.recursive), + include_imports: convertStringBoolean().describe(RAW_SECRETS.LIST.includeImports), tagSlugs: z .string() .describe(RAW_SECRETS.LIST.tagSlugs) @@ -274,15 +251,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: secretRawSchema .extend({ secretPath: z.string().optional(), + secretValueHidden: z.boolean(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + tags: SanitizedTagSchema.array().optional() }) .array(), imports: z @@ -293,6 +264,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: secretRawSchema .omit({ createdAt: true, updatedAt: true }) .extend({ + secretValueHidden: z.boolean(), secretMetadata: ResourceMetadataSchema.optional() }) .array() @@ -342,6 +314,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { expandSecretReferences: req.query.expandSecretReferences, actorAuthMethod: req.permission.authMethod, projectId: workspaceId, + viewSecretValue: req.query.viewSecretValue, path: secretPath, metadataFilter: req.query.metadataFilter, includeImports: req.query.include_imports, @@ -376,6 +349,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); } + return { secrets, imports }; } }); @@ -445,28 +419,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath), version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version), type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type), - expandSecretReferences: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.GET.expand), - include_imports: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.GET.includeImports) + viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.GET.viewSecretValue), + expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.GET.expand), + include_imports: convertStringBoolean().describe(RAW_SECRETS.GET.includeImports) }), response: { 200: z.object({ secret: secretRawSchema.extend({ - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional(), + secretValueHidden: z.boolean(), + tags: SanitizedTagSchema.array().optional(), secretMetadata: ResourceMetadataSchema.optional() }) }) @@ -498,6 +459,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { expandSecretReferences: req.query.expandSecretReferences, environment, projectId: workspaceId, + viewSecretValue: req.query.viewSecretValue, projectSlug: workspaceSlug, path: secretPath, secretName: req.params.secretName, @@ -704,7 +666,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secret: secretRawSchema + secret: secretRawSchema.extend({ + secretValueHidden: z.boolean() + }) }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -800,7 +764,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secret: secretRawSchema + secret: secretRawSchema.extend({ + secretValueHidden: z.boolean() + }) }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -822,6 +788,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { if (secretOperation.type === SecretProtectionType.Approval) { return { approval: secretOperation.approval }; } + const { secret } = secretOperation; await server.services.auditLog.createAuditLog({ @@ -884,13 +851,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { workspace: z.string(), environment: z.string(), secretPath: z.string().optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() + tags: SanitizedTagSchema.array() }) .array(), imports: z @@ -986,10 +947,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: z.string().trim().default("/").transform(removeTrailingSlash), type: z.nativeEnum(SecretType).default(SecretType.Shared), version: z.coerce.number().optional(), - include_imports: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") + include_imports: convertStringBoolean() }), response: { 200: z.object({ @@ -1260,6 +1218,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { z.object({ secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( z.object({ + secretValueHidden: z.boolean(), _id: z.string(), workspace: z.string(), environment: z.string() @@ -1429,13 +1388,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( - z.object({ - _id: z.string(), - workspace: z.string(), - environment: z.string() - }) - ) + secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ + _id: z.string(), + secretValueHidden: z.boolean(), + workspace: z.string(), + environment: z.string() + }) }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -1747,7 +1705,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() + secrets: SecretsSchema.omit({ secretBlindIndex: true }).extend({ secretValueHidden: z.boolean() }).array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -1862,7 +1820,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() + secrets: SecretsSchema.omit({ secretBlindIndex: true }) + .extend({ + secretValueHidden: z.boolean() + }) + .array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -2124,7 +2086,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: secretRawSchema.array() + secrets: secretRawSchema.extend({ secretValueHidden: z.boolean() }).array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -2246,7 +2208,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: secretRawSchema.array() + secrets: secretRawSchema + .extend({ + secretValueHidden: z.boolean() + }) + .array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) diff --git a/backend/src/services/external-migration/external-migration-fns.ts b/backend/src/services/external-migration/external-migration-fns.ts index f4a54f0db..856b39012 100644 --- a/backend/src/services/external-migration/external-migration-fns.ts +++ b/backend/src/services/external-migration/external-migration-fns.ts @@ -31,9 +31,9 @@ export type TImportDataIntoInfisicalDTO = { projectEnvDAL: Pick; kmsService: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; resourceMetadataDAL: Pick; diff --git a/backend/src/services/external-migration/external-migration-queue.ts b/backend/src/services/external-migration/external-migration-queue.ts index e3d77e832..8aa46b94c 100644 --- a/backend/src/services/external-migration/external-migration-queue.ts +++ b/backend/src/services/external-migration/external-migration-queue.ts @@ -27,9 +27,9 @@ export type TExternalMigrationQueueFactoryDep = { projectEnvDAL: Pick; kmsService: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; folderDAL: Pick; diff --git a/backend/src/services/integration-auth/integration-delete-secret.ts b/backend/src/services/integration-auth/integration-delete-secret.ts index fdefd0e62..4b07245ac 100644 --- a/backend/src/services/integration-auth/integration-delete-secret.ts +++ b/backend/src/services/integration-auth/integration-delete-secret.ts @@ -68,7 +68,8 @@ const getIntegrationSecretsV2 = async ( secretDAL: secretV2BridgeDAL, secretImportDAL, secretImports, - hasSecretAccess: () => true + hasSecretAccess: () => true, + viewSecretValue: true }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index 35994433a..f72357964 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -2,7 +2,11 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { NotFoundError } from "@app/lib/errors"; import { TProjectPermission } from "@app/lib/types"; @@ -92,7 +96,7 @@ export const integrationServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath @@ -175,7 +179,7 @@ export const integrationServiceFactory = ({ if (environment || secretPath) { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: newEnvironment, secretPath: newSecretPath diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 4b110ebd2..5367a70cd 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -11,7 +11,11 @@ import { import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types"; import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; @@ -760,7 +764,10 @@ export const projectServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSub.Secrets + ); const project = await projectDAL.findProjectById(projectId); diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index d21ad3b9c..e5a450441 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -3,6 +3,7 @@ import { groupBy, unique } from "@app/lib/fn"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { TSecretDALFactory } from "../secret/secret-dal"; +import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretImportDALFactory } from "./secret-import-dal"; @@ -32,6 +33,12 @@ type TSecretImportSecretsV2 = { folderId: string | undefined; importFolderId: string; secrets: (TSecretsV2 & { + secretTags: { + slug: string; + name: string; + color?: string | null; + id: string; + }[]; workspace: string; environment: string; _id: string; @@ -39,6 +46,7 @@ type TSecretImportSecretsV2 = { // akhilmhdh: yes i know you can put ?. // But for somereason ts consider ? and undefined explicit as different just ts things secretValue: string; + secretValueHidden: boolean; secretComment: string; secretMetadata?: ResourceMetadataDTO; })[]; @@ -150,12 +158,14 @@ export const fnSecretsV2FromImports = async ({ secretImportDAL, decryptor, expandSecretReferences, - hasSecretAccess + hasSecretAccess, + viewSecretValue }: { secretImports: (Omit & { importEnv: { id: string; slug: string; name: string }; })[]; folderDAL: Pick; + viewSecretValue: boolean; secretDAL: Pick; secretImportDAL: Pick; decryptor: (value?: Buffer | null) => string; @@ -168,9 +178,14 @@ export const fnSecretsV2FromImports = async ({ hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean; }) => { const cyclicDetector = new Set(); - const stack: { secretImports: typeof rootSecretImports; depth: number; parentImportedSecrets: TSecretsV2[] }[] = [ - { secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] } - ]; + const stack: { + secretImports: typeof rootSecretImports; + depth: number; + parentImportedSecrets: (TSecretsV2 & { + secretValueHidden: boolean; + secretTags: { slug: string; name: string; id: string; color?: string | null }[]; + })[]; + }[] = [{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }]; const processedImports: TSecretImportSecretsV2[] = []; @@ -229,7 +244,9 @@ export const fnSecretsV2FromImports = async ({ .map((item) => ({ ...item, secretKey: item.key, - secretValue: decryptor(item.encryptedValue), + secretValue: viewSecretValue ? decryptor(item.encryptedValue) : INFISICAL_SECRET_VALUE_HIDDEN_MASK, + secretValueHidden: !viewSecretValue, + secretTags: item.tags, secretComment: decryptor(item.encryptedComment), environment: importEnv.slug, workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend. @@ -267,6 +284,8 @@ export const fnSecretsV2FromImports = async ({ processedImport.secrets = unique(processedImport.secrets, (i) => i.key); return Promise.allSettled( processedImport.secrets.map(async (decryptedSecret, index) => { + if (decryptedSecret.secretValueHidden) return; + const expandedSecretValue = await expandSecretReferences({ value: decryptedSecret.secretValue, secretPath: processedImport.secretPath, diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index e8fde04d1..6c412edc0 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -5,7 +5,11 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType, TableName } 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 { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -90,7 +94,7 @@ export const secretImportServiceFactory = ({ // check if user has permission to import from target path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment: data.environment, secretPath: data.path @@ -402,7 +406,7 @@ export const secretImportServiceFactory = ({ // check if user has permission to import from target path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment: secretImportDoc.importEnv.slug, secretPath: secretImportDoc.importPath @@ -596,7 +600,7 @@ export const secretImportServiceFactory = ({ const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false }); const allowedImports = secretImports.filter((el) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: el.importEnv.slug, secretPath: el.importPath @@ -642,12 +646,13 @@ export const secretImportServiceFactory = ({ const importedSecrets = await fnSecretsV2FromImports({ secretImports, folderDAL, + viewSecretValue: true, secretDAL: secretV2BridgeDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, @@ -667,7 +672,7 @@ export const secretImportServiceFactory = ({ const allowedImports = secretImports.filter((el) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: el.importEnv.slug, secretPath: el.importPath @@ -683,7 +688,10 @@ export const secretImportServiceFactory = ({ return importedSecrets.map((el) => ({ ...el, secrets: el.secrets.map((encryptedSecret) => - decryptSecretRaw({ ...encryptedSecret, workspace: projectId, environment, secretPath }, botKey) + decryptSecretRaw( + { ...encryptedSecret, workspace: projectId, environment, secretPath, secretValueHidden: false }, + botKey + ) ) })); }; diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index cdc9540da..8afcf6416 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -249,7 +249,8 @@ export const secretSyncQueueFactory = ({ expandSecretReferences, secretImportDAL, secretImports, - hasSecretAccess: () => true + hasSecretAccess: () => true, + viewSecretValue: true }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index 8211180e8..7b92a463c 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -3,7 +3,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { - ProjectPermissionActions, + ProjectPermissionSecretActions, ProjectPermissionSecretSyncActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -179,7 +179,7 @@ export const secretSyncServiceFactory = ({ ); ForbiddenError.from(projectPermission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath @@ -270,7 +270,7 @@ export const secretSyncServiceFactory = ({ throw new BadRequestError({ message: "Must specify both source environment and secret path" }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: updatedEnvironment, secretPath: updatedSecretPath diff --git a/backend/src/services/secret-tag/secret-tag-dal.ts b/backend/src/services/secret-tag/secret-tag-dal.ts index 1df64afa2..3b9151557 100644 --- a/backend/src/services/secret-tag/secret-tag-dal.ts +++ b/backend/src/services/secret-tag/secret-tag-dal.ts @@ -47,6 +47,7 @@ export const secretTagDALFactory = (db: TDbClient) => { throw new DatabaseError({ error, name: "Find all by ids" }); } }; + return { ...secretTagOrm, saveTagsToSecret: secretJnTagOrm.insertMany, diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 751235cde..8e1de2d91 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -8,6 +8,7 @@ import { logger } from "@app/lib/logger"; import { ActorType } from "../auth/auth-type"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; +import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types"; @@ -108,6 +109,7 @@ export const fnSecretBulkInsert = async ({ [`${TableName.SecretV2}Id` as const]: newSecretGroupedByKeyName[key][0].id })) ); + const secretVersions = await secretVersionDAL.insertMany( sanitizedInputSecrets.map((el) => ({ ...el, @@ -146,6 +148,7 @@ export const fnSecretBulkInsert = async ({ if (newSecretTags.length) { const secTags = await secretTagDAL.saveTagsToSecretV2(newSecretTags, tx); const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId); + const newSecretVersionTags = secTags.flatMap(({ secrets_v2Id, secret_tagsId }) => ({ [`${TableName.SecretVersionV2}Id` as const]: secVersionsGroupBySecId[secrets_v2Id][0].id, [`${TableName.SecretTag}Id` as const]: secret_tagsId @@ -154,7 +157,16 @@ export const fnSecretBulkInsert = async ({ await secretVersionTagDAL.insertMany(newSecretVersionTags, tx); } - return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); + const secretsWithTags = await secretDAL.find( + { + $in: { + [`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id) + } + }, + { tx } + ); + + return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id })); }; export const fnSecretBulkUpdate = async ({ @@ -300,7 +312,15 @@ export const fnSecretBulkUpdate = async ({ tx ); - return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); + const secretsWithTags = await secretDAL.find( + { + $in: { + [`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id) + } + }, + { tx } + ); + return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id })); }; export const fnSecretBulkDelete = async ({ @@ -533,7 +553,7 @@ export const expandSecretReferencesFactory = ({ const referredValue = await fetchSecret(environment, secretPath, secretKey); if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags)) throw new ForbiddenRequestError({ - message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to.` + message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to read value on.` }); const cacheKey = getCacheUniqueKey(environment, secretPath); @@ -552,7 +572,7 @@ export const expandSecretReferencesFactory = ({ const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags)) throw new ForbiddenRequestError({ - message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to.` + message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to read value on.` }); const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath); @@ -646,13 +666,13 @@ export const reshapeBridgeSecret = ( name: string; }[]; secretMetadata?: ResourceMetadataDTO; - } + }, + secretValueHidden: boolean ) => ({ secretKey: secret.key, secretPath, workspace: workspaceId, environment, - secretValue: secret.value || "", secretComment: secret.comment || "", version: secret.version, type: secret.type, @@ -674,5 +694,15 @@ export const reshapeBridgeSecret = ( metadata: secret.metadata, secretMetadata: secret.secretMetadata, createdAt: secret.createdAt, - updatedAt: secret.updatedAt + updatedAt: secret.updatedAt, + + ...(secretValueHidden + ? { + secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK, + secretValueHidden: true + } + : { + secretValue: secret.value || "", + secretValueHidden: false + }) }); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index d6ba02856..3b7f04907 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -11,7 +11,11 @@ import { TSecretsV2 } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; @@ -259,7 +263,7 @@ export const secretV2BridgeServiceFactory = ({ const { secretName, type, ...inputSecretData } = inputSecret; ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -278,8 +282,8 @@ export const secretV2BridgeServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); - const secret = await secretDAL.transaction((tx) => - fnSecretBulkInsert({ + const secret = await secretDAL.transaction(async (tx) => { + const [createdSecret] = await fnSecretBulkInsert({ folderId, orgId: actorOrgId, inputSecrets: [ @@ -313,8 +317,10 @@ export const secretV2BridgeServiceFactory = ({ actorId }, tx - }) - ); + }); + + return createdSecret; + }); if (inputSecret.type === SecretType.Shared) { await snapshotService.performSnapshot(folderId); @@ -328,11 +334,17 @@ export const secretV2BridgeServiceFactory = ({ }); } - return reshapeBridgeSecret(projectId, environment, secretPath, { - ...secret[0], - value: inputSecret.secretValue, - comment: inputSecret.secretComment || "" - }); + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...secret, + value: inputSecret.secretValue, + comment: inputSecret.secretComment || "" + }, + false + ); }; const updateSecret = async ({ @@ -401,7 +413,7 @@ export const secretV2BridgeServiceFactory = ({ } ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -412,18 +424,22 @@ export const secretV2BridgeServiceFactory = ({ // validate tags // fetch all tags and if not same count throw error meaning one was invalid tags - const tags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : []; - if ((inputSecret.tagIds || []).length !== tags.length) - throw new NotFoundError({ message: `Tag not found. Found ${tags.map((el) => el.slug).join(",")}` }); + const newTags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : []; + if ((inputSecret.tagIds || []).length !== newTags.length) + throw new NotFoundError({ message: `Tag not found. Found ${newTags.map((el) => el.slug).join(",")}` }); + + const tagsToCheck = inputSecret.tagIds ? newTags : secret.tags; // now check with new ids ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: inputSecret.secretName, - secretTags: tags?.map((el) => el.slug) + ...(tagsToCheck.length && { + secretTags: tagsToCheck.map((el) => el.slug) + }) }) ); @@ -435,12 +451,14 @@ export const secretV2BridgeServiceFactory = ({ }); if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exist" }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: inputSecret.newSecretName, - secretTags: tags?.map((el) => el.slug) + ...(tagsToCheck.length && { + secretTags: tagsToCheck.map((el) => el.slug) + }) }) ); } @@ -522,11 +540,29 @@ export const secretV2BridgeServiceFactory = ({ }); } - return reshapeBridgeSecret(projectId, environment, secretPath, { - ...updatedSecret[0], - value: inputSecret.secretValue || "", - comment: inputSecret.secretComment || "" - }); + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: inputSecret.secretName, + ...(tagsToCheck.length && { + secretTags: tagsToCheck.map((el) => el.slug) + }) + }) + ); + + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...updatedSecret[0], + value: inputSecret.secretValue || "", + comment: inputSecret.secretComment || "" + }, + secretValueHidden + ); }; const deleteSecret = async ({ @@ -572,7 +608,7 @@ export const secretV2BridgeServiceFactory = ({ }); if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -614,15 +650,32 @@ export const secretV2BridgeServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); - return reshapeBridgeSecret(projectId, environment, secretPath, { - ...deletedSecret[0], - value: deletedSecret[0].encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString() - : "", - comment: deletedSecret[0].encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString() - : "" - }); + + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: secretToDelete.key, + secretTags: secretToDelete.tags?.map((el) => el.slug) + }) + ); + + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...deletedSecret[0], + value: deletedSecret[0].encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString() + : "", + comment: deletedSecret[0].encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString() + : "" + }, + secretValueHidden + ); }; // get unique secrets count for multiple envs @@ -650,7 +703,10 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSub.Secrets + ); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -697,7 +753,10 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSub.Secrets + ); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) return 0; @@ -708,7 +767,13 @@ export const secretV2BridgeServiceFactory = ({ }; const getSecretsByFolderMappings = async ( - { projectId, userId, filters, folderMappings }: TGetSecretsRawByFolderMappingsDTO, + { + projectId, + userId, + filters, + folderMappings, + filterByAction = ProjectPermissionSecretActions.ReadValue + }: TGetSecretsRawByFolderMappingsDTO, projectPermission: Awaited>["permission"] ) => { const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId); @@ -728,7 +793,7 @@ export const secretV2BridgeServiceFactory = ({ const decryptedSecrets = secrets .filter((el) => projectPermission.can( - ProjectPermissionActions.Read, + filterByAction, subject(ProjectPermissionSub.Secrets, { environment: groupedFolderMappings[el.folderId][0].environment, secretPath: groupedFolderMappings[el.folderId][0].path, @@ -737,8 +802,19 @@ export const secretV2BridgeServiceFactory = ({ }) ) ) - .map((secret) => - reshapeBridgeSecret( + .map((secret) => { + // Note(Daniel): This is only relevant if the filterAction isn't set to ReadValue. This is needed for the frontend. + const secretValueHidden = !projectPermission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: groupedFolderMappings[secret.folderId][0].environment, + secretPath: groupedFolderMappings[secret.folderId][0].path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }) + ); + + return reshapeBridgeSecret( projectId, groupedFolderMappings[secret.folderId][0].environment, groupedFolderMappings[secret.folderId][0].path, @@ -750,9 +826,10 @@ export const secretV2BridgeServiceFactory = ({ comment: secret.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() : "" - } - ) - ); + }, + secretValueHidden + ); + }); return decryptedSecrets; }; @@ -781,7 +858,10 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); if (!isInternal) { - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSub.Secrets + ); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -801,7 +881,8 @@ export const secretV2BridgeServiceFactory = ({ projectId, folderMappings, filters: params, - userId: actorId + userId: actorId, + filterByAction: ProjectPermissionSecretActions.DescribeSecret }, permission ); @@ -816,10 +897,12 @@ export const secretV2BridgeServiceFactory = ({ projectId, actor, actorOrgId, + viewSecretValue, actorAuthMethod, includeImports, recursive, expandSecretReferences: shouldExpandSecretReferences, + throwOnMissingReadValuePermission = true, ...params }: TGetSecretsDTO) => { const { permission } = await permissionService.getProjectPermission({ @@ -831,7 +914,14 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeSecret, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: path, + secretTags: params.tagSlugs + }) + ); let paths: { folderId: string; path: string }[] = []; @@ -869,28 +959,87 @@ export const secretV2BridgeServiceFactory = ({ }); const decryptedSecrets = secrets - .filter((el) => - permission.can( - ProjectPermissionActions.Read, + .filter((el) => { + const canDescribeSecret = permission.can( + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath: groupedPaths[el.folderId][0].path, secretName: el.key, secretTags: el.tags.map((i) => i.slug) }) - ) - ) - .map((secret) => - reshapeBridgeSecret(projectId, environment, groupedPaths[secret.folderId][0].path, { - ...secret, - value: secret.encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() - : "", - comment: secret.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() - : "" - }) - ); + ); + + if (!canDescribeSecret) { + return false; + } + + if (viewSecretValue) { + // Recursive secret, should be filtered out + if (groupedPaths[el.folderId][0].path !== path) { + const canReadRecursiveSecretValue = permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ); + + if (!canReadRecursiveSecretValue) { + return false; + } + } + + if (throwOnMissingReadValuePermission) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ); + } + // Else, we do nothing. Because we don't want to filter out the secret, OR throw an error. + // If the user doesn't have access to read the value, in the below map function, we mask the secret value and return the secret with a hidden value. + } + + return canDescribeSecret; + }) + .map((secret) => { + const isPersonalSecret = secret.userId === actorId && secret.type === SecretType.Personal; + + const secretValueHidden = + !viewSecretValue || + !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: groupedPaths[secret.folderId][0].path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }) + ); + + return reshapeBridgeSecret( + projectId, + environment, + groupedPaths[secret.folderId][0].path, + { + ...secret, + value: secret.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() + : "", + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + }, + secretValueHidden && !isPersonalSecret + ); + }); const { expandSecretReferences } = expandSecretReferencesFactory({ projectId, @@ -899,7 +1048,7 @@ export const secretV2BridgeServiceFactory = ({ decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, @@ -938,22 +1087,36 @@ export const secretV2BridgeServiceFactory = ({ const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId)); const allowedImports = secretImports.filter(({ isReplication }) => !isReplication); const importedSecrets = await fnSecretsV2FromImports({ + viewSecretValue, secretImports: allowedImports, secretDAL, folderDAL, secretImportDAL, expandSecretReferences, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), - hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionActions.Read, + hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { + const canDescribe = permission.can( + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, secretTags: expandSecretTags }) - ) + ); + + const canReadValue = permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) + ); + + return viewSecretValue ? canDescribe && canReadValue : canDescribe; + } }); return { @@ -1040,6 +1203,7 @@ export const secretV2BridgeServiceFactory = ({ type, secretName, version, + viewSecretValue, includeImports, expandSecretReferences: shouldExpandSecretReferences }: TGetASecretDTO) => { @@ -1101,7 +1265,7 @@ export const secretV2BridgeServiceFactory = ({ )); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, @@ -1110,56 +1274,93 @@ export const secretV2BridgeServiceFactory = ({ }) ); + // this will throw if the user doesn't have read value permission no matter what + // because if its an expansion, it will fully depend on the value. const { expandSecretReferences } = expandSecretReferencesFactory({ projectId, folderDAL, secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), - canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionActions.Read, + canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { + return permission.can( + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, secretTags: expandSecretTags }) - ) + ); + } }); // now if secret is not found // then search for imported secrets // here we consider the import order also thus starting from bottom + + // currently filters out the secrets that the user doesn't have access to read value on if (!secret && includeImports) { const secretImports = await secretImportDAL.find({ folderId, isReplication: false }); const importedSecrets = await fnSecretsV2FromImports({ secretImports, + viewSecretValue, secretDAL, folderDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined, - hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionActions.Read, + hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { + return permission.can( + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, secretTags: expandSecretTags }) - ) + ); + } }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) { const importedSecret = importedSecrets[i].secrets[j]; if (secretName === importedSecret.key) { - return reshapeBridgeSecret(projectId, importedSecrets[i].environment, importedSecrets[i].secretPath, { - ...importedSecret, - value: importedSecret.secretValue || "", - comment: importedSecret.secretComment || "" - }); + let secretValueHidden = true; + + if (viewSecretValue) { + if ( + !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: importedSecret.environment, + secretPath: importedSecrets[i].secretPath, + secretName: importedSecret.key, + secretTags: (importedSecret.secretTags || []).map((el) => el.slug) + }) + ) && + secretType !== SecretType.Personal + ) { + throw new ForbiddenRequestError({ + message: `You do not have permission to view secret import value on secret with name '${secretName}'`, + name: "ForbiddenReadSecretError" + }); + } + + secretValueHidden = false; + } + + return reshapeBridgeSecret( + projectId, + importedSecrets[i].environment, + importedSecrets[i].secretPath, + { + ...importedSecret, + value: importedSecret.secretValue || "", + comment: importedSecret.secretComment || "" + }, + secretValueHidden + ); } } } @@ -1181,13 +1382,43 @@ export const secretV2BridgeServiceFactory = ({ secretValue = expandedSecretValue || ""; } - return reshapeBridgeSecret(projectId, environment, path, { - ...secret, - value: secretValue, - comment: secret.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() - : "" - }); + let secretValueHidden = true; + + if (viewSecretValue) { + if ( + !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: path, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }) + ) && + secretType !== SecretType.Personal + ) { + throw new ForbiddenRequestError({ + message: `You do not have permission to view secret value on secret with name '${secretName}'`, + name: "ForbiddenReadSecretError" + }); + } + + secretValueHidden = false; + } + + return reshapeBridgeSecret( + projectId, + environment, + path, + { + ...secret, + value: secretValue, + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + }, + secretValueHidden + ); }; const createManySecret = async ({ @@ -1255,7 +1486,7 @@ export const secretV2BridgeServiceFactory = ({ inputSecrets.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1330,13 +1561,29 @@ export const secretV2BridgeServiceFactory = ({ environmentSlug: folder.environment.slug }); - return newSecrets.map((el) => - reshapeBridgeSecret(projectId, environment, secretPath, { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }) - ); + return newSecrets.map((el) => { + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags?.map((i) => i.slug) + }) + ); + + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" + }, + secretValueHidden + ); + }); }; const updateManySecret = async ({ @@ -1379,7 +1626,17 @@ export const secretV2BridgeServiceFactory = ({ const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); - const updatedSecrets: Array = []; + const updatedSecrets: Array< + TSecretsV2 & { + secretPath: string; + tags: { + id: string; + slug: string; + color?: string | null; + name: string; + }[]; + } + > = []; await secretDAL.transaction(async (tx) => { for await (const folder of folders) { if (!folder) throw new NotFoundError({ message: "Folder not found" }); @@ -1430,7 +1687,7 @@ export const secretV2BridgeServiceFactory = ({ secretsToUpdateInDB.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1450,7 +1707,7 @@ export const secretV2BridgeServiceFactory = ({ if (updateMode === SecretUpdateMode.Upsert) { secretsToCreate.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1464,7 +1721,7 @@ export const secretV2BridgeServiceFactory = ({ // check again to avoid non authorized tags are removed secretsToUpdate.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1516,7 +1773,7 @@ export const secretV2BridgeServiceFactory = ({ secretsWithNewName.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1582,6 +1839,7 @@ export const secretV2BridgeServiceFactory = ({ }, resourceMetadataDAL }); + updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); if (updateMode === SecretUpdateMode.Upsert) { const bulkInsertedSecrets = await fnSecretBulkInsert({ @@ -1618,6 +1876,7 @@ export const secretV2BridgeServiceFactory = ({ }, tx }); + updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); } } @@ -1639,13 +1898,33 @@ export const secretV2BridgeServiceFactory = ({ ) ); - return updatedSecrets.map((el) => - reshapeBridgeSecret(projectId, environment, el.secretPath, { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }) - ); + return updatedSecrets.map((el) => { + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: el.secretPath, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ); + + return { + ...reshapeBridgeSecret( + projectId, + environment, + el.secretPath, + { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() + : "" + }, + secretValueHidden + ) + }; + }); }; const deleteManySecret = async ({ @@ -1707,7 +1986,7 @@ export const secretV2BridgeServiceFactory = ({ }); secretsToDelete.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1746,13 +2025,35 @@ export const secretV2BridgeServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); - return secretsDeleted.map((el) => - reshapeBridgeSecret(projectId, environment, secretPath, { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }) - ); + return secretsDeleted.map((el) => { + const secretToDeleteMatch = secretsToDelete.find( + (i) => i.key === el.key && (i.type || SecretType.Shared) === el.type + ); + + const secretValueHidden = + !secretToDeleteMatch || + !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.key, + secretTags: secretToDeleteMatch.tags?.map((i) => i.slug) + }) + ); + + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" + }, + secretValueHidden + ); + }); }; const getSecretVersions = async ({ @@ -1765,11 +2066,18 @@ export const secretV2BridgeServiceFactory = ({ secretId }: TGetSecretVersionsDTO) => { const secret = await secretDAL.findById(secretId); + if (!secret) throw new NotFoundError({ message: `Secret with ID '${secretId}' not found` }); const folder = await folderDAL.findById(secret.folderId); if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` }); + const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]); + + if (!folderWithPath) { + throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` }); + } + const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -1788,13 +2096,29 @@ export const secretV2BridgeServiceFactory = ({ limit, sort: [["createdAt", "desc"]] }); - return secretVersions.map((el) => { - return reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }); + const secretValueHidden = permission.cannot( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: folder.environment.envSlug, + secretPath: folderWithPath.path, + secretName: el.key, + ...(el.tags?.length && { + secretTags: el.tags.map((tag) => tag.slug) + }) + }) + ); + return reshapeBridgeSecret( + folder.projectId, + folder.environment.envSlug, + folderWithPath.path, + { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" + }, + secretValueHidden + ); }); }; @@ -1890,16 +2214,26 @@ export const secretV2BridgeServiceFactory = ({ [`${TableName.SecretV2}.id` as "id"]: secretIds } }); + + const sourceActions = [ + ProjectPermissionSecretActions.Delete, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.DescribeSecret + ] as const; + const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const; + sourceSecrets.forEach((secret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { - environment: sourceEnvironment, - secretPath: sourceSecretPath, - secretName: secret.key, - secretTags: secret.tags.map((el) => el.slug) - }) - ); + for (const sourceAction of sourceActions) { + ForbiddenError.from(permission).throwUnlessCan( + sourceAction, + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + } }); if (sourceSecrets.length !== secretIds.length) { @@ -1974,27 +2308,17 @@ export const secretV2BridgeServiceFactory = ({ // permission check whether can create or edit the ones in the destination folder locallyCreatedSecrets.forEach((secret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationEnvironment, - secretName: secret.key, - secretTags: secret.tags.map((el) => el.slug) - }) - ); - }); - - locallyUpdatedSecrets.forEach((secret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationEnvironment, - secretName: secret.key, - secretTags: secret.tags.map((el) => el.slug) - }) - ); + for (const destinationAction of destinationActions) { + ForbiddenError.from(permission).throwUnlessCan( + destinationAction, + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationFolder.path, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + } }); const destinationFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy( @@ -2232,7 +2556,7 @@ export const secretV2BridgeServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -2256,7 +2580,7 @@ export const secretV2BridgeServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -2265,7 +2589,7 @@ export const secretV2BridgeServiceFactory = ({ }) ); - const secretValue = secret.encryptedValue + const decryptedSecretValue = secret.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() : ""; @@ -2276,7 +2600,7 @@ export const secretV2BridgeServiceFactory = ({ decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretName, expandSecretTags) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath, @@ -2286,10 +2610,26 @@ export const secretV2BridgeServiceFactory = ({ ) }); + if ( + !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }) + ) + ) { + throw new ForbiddenRequestError({ + message: `Unable to get secret reference tree for secret with key '${secretName}', because you don't have permission to view secret value.` + }); + } + const { expandedValue, stackTrace } = await getExpandedSecretStackTrace({ environment, secretPath, - value: secretValue + value: decryptedSecretValue }); return { tree: stackTrace, value: expandedValue }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index 22956463d..510181d0c 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -1,6 +1,7 @@ import { Knex } from "knex"; import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas"; +import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; @@ -36,6 +37,8 @@ export type TGetSecretsDTO = { includeImports?: boolean; recursive?: boolean; tagSlugs?: string[]; + viewSecretValue: boolean; + throwOnMissingReadValuePermission?: boolean; metadataFilter?: { key?: string; value?: string; @@ -48,6 +51,11 @@ export type TGetSecretsDTO = { keys?: string[]; } & TProjectPermission; +export type TGetSecretsMissingReadValuePermissionDTO = Omit< + TGetSecretsDTO, + "viewSecretValue" | "recursive" | "expandSecretReferences" +>; + export type TGetASecretDTO = { secretName: string; path: string; @@ -57,6 +65,7 @@ export type TGetASecretDTO = { includeImports?: boolean; version?: number; projectId: string; + viewSecretValue: boolean; } & Omit; export type TCreateSecretDTO = TProjectPermission & { @@ -164,9 +173,9 @@ export type TFnSecretBulkInsert = { } >; resourceMetadataDAL: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; actor?: { type: string; @@ -192,9 +201,9 @@ export type TFnSecretBulkUpdate = { data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO }; }[]; resourceMetadataDAL: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; actor?: { type: string; @@ -340,4 +349,5 @@ export type TGetSecretsRawByFolderMappingsDTO = { folderMappings: { folderId: string; path: string; environment: string }[]; userId: string; filters: TFindSecretsByFolderIdsFilter; + filterByAction?: ProjectPermissionSecretActions; }; diff --git a/backend/src/services/secret-v2-bridge/secret-version-dal.ts b/backend/src/services/secret-v2-bridge/secret-version-dal.ts index d06aa1472..12c54cd7c 100644 --- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts @@ -2,9 +2,9 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas"; +import { SecretVersionsV2Schema, TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex"; +import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; import { QueueName } from "@app/queue"; @@ -13,6 +13,58 @@ export type TSecretVersionV2DALFactory = ReturnType { const secretVersionV2Orm = ormify(db, TableName.SecretVersionV2); + const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt = {}) => { + try { + const query = (tx || db.replicaNode())(TableName.SecretVersionV2) + .where(`${TableName.SecretVersionV2}.secretId`, secretId) + .leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`) + .leftJoin( + TableName.SecretV2JnTag, + `${TableName.SecretV2}.id`, + `${TableName.SecretV2JnTag}.${TableName.SecretV2}Id` + ) + .leftJoin( + TableName.SecretTag, + `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, + `${TableName.SecretTag}.id` + ) + .select(selectAllTableCols(TableName.SecretVersionV2)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); + } + + const docs = await query; + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretVersionsV2Schema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + + return data; + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.SecretVersionV2}: FindBySecretId` }); + } + }; + // This will fetch all latest secret versions from a folder const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { try { @@ -181,6 +233,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { findLatestVersionMany, bulkUpdate, findLatestVersionByFolderId, - findVersionsBySecretIdWithActors + findVersionsBySecretIdWithActors, + findBySecretId }; }; diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts index cbaf7ddcd..dc41d129f 100644 --- a/backend/src/services/secret/secret-dal.ts +++ b/backend/src/services/secret/secret-dal.ts @@ -169,6 +169,48 @@ export const secretDALFactory = (db: TDbClient) => { } }; + const findManySecretsWithTags = async ( + filter: { + secretIds: string[]; + type: SecretType; + }, + tx?: Knex + ) => { + try { + const secrets = await (tx || db.replicaNode())(TableName.Secret) + .whereIn(`${TableName.Secret}.id` as "id", filter.secretIds) + .where("type", filter.type) + .leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`) + .leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`) + .select(selectAllTableCols(TableName.Secret)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + + const data = sqlNestRelationships({ + data: secrets, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + + return data; + } catch (error) { + throw new DatabaseError({ error, name: "get many secrets with tags" }); + } + }; + const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => { try { // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo) @@ -443,6 +485,7 @@ export const secretDALFactory = (db: TDbClient) => { upsertSecretReferences, findReferencedSecretReferences, findAllProjectSecretValues, - pruneSecretReminders + pruneSecretReminders, + findManySecretsWithTags }; }; diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index d7c9b86fb..4556528a8 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -13,7 +13,7 @@ import { TSecrets } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { buildSecretBlindIndexFromName, @@ -51,6 +51,8 @@ import { TUpdateManySecretsRawFnFactory } from "./secret-types"; +export const INFISICAL_SECRET_VALUE_HIDDEN_MASK = ""; + export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => { const appCfg = getConfig(); const secretBlindIndex = await buildSecretBlindIndexFromName({ @@ -190,7 +192,7 @@ export const recursivelyGetSecretPaths = ({ const allowedPaths = paths.filter( (folder) => permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath: folder.path @@ -344,6 +346,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD export const decryptSecretRaw = ( secret: TSecrets & { + secretValueHidden: boolean; workspace: string; environment: string; secretPath: string; @@ -362,12 +365,14 @@ export const decryptSecretRaw = ( key }); - const secretValue = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key - }); + const secretValue = !secret.secretValueHidden + ? decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key + }) + : INFISICAL_SECRET_VALUE_HIDDEN_MASK; let secretComment = ""; @@ -385,6 +390,7 @@ export const decryptSecretRaw = ( secretPath: secret.secretPath, workspace: secret.workspace, environment: secret.environment, + secretValueHidden: secret.secretValueHidden, secretValue, secretComment, version: secret.version, @@ -1198,3 +1204,23 @@ export const fnDeleteProjectSecretReminders = async ( } } }; + +export const conditionallyHideSecretValue = ( + shouldHideValue: boolean, + { + secretValueCiphertext, + secretValueIV, + secretValueTag + }: { + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + } +) => { + return { + secretValueCiphertext: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueCiphertext, + secretValueIV: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueIV, + secretValueTag: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueTag, + secretValueHidden: shouldHideValue + }; +}; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index c84fe5ae0..76a486b46 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -403,7 +403,8 @@ export const secretQueueFactory = ({ expandSecretReferences, secretImportDAL, secretImports, - hasSecretAccess: () => true + hasSecretAccess: () => true, + viewSecretValue: true }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index cfb47d1dd..8af0deb2b 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -6,6 +6,7 @@ import { ActionProjectType, ProjectMembershipRole, ProjectUpgradeStatus, + ProjectVersion, SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, @@ -13,7 +14,11 @@ import { } 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 { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; @@ -48,6 +53,7 @@ import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bri import { TGetSecretReferencesTreeDTO } from "../secret-v2-bridge/secret-v2-bridge-types"; import { TSecretDALFactory } from "./secret-dal"; import { + conditionallyHideSecretValue, decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkDelete, @@ -205,7 +211,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -323,7 +329,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -445,7 +451,22 @@ export const secretServiceFactory = ({ environmentSlug: folder.environment.slug }); } - return { ...updatedSecret[0], workspace: projectId, environment, secretPath: path }; + + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: path + }) + ); + + return { + ...updatedSecret[0], + ...conditionallyHideSecretValue(secretValueHidden, updatedSecret[0]), + workspace: projectId, + environment, + secretPath: path + }; }; const deleteSecret = async ({ @@ -468,7 +489,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -541,7 +562,19 @@ export const secretServiceFactory = ({ }); } - return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment, secretPath: path }; + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + ); + + return { + ...deletedSecret[0], + ...conditionallyHideSecretValue(secretValueHidden, deletedSecret[0]), + _id: deletedSecret[0].id, + workspace: projectId, + environment, + secretPath: path + }; }; const getSecrets = async ({ @@ -590,7 +623,7 @@ export const secretServiceFactory = ({ paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p })); } else { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -615,7 +648,7 @@ export const secretServiceFactory = ({ actor === ActorType.SERVICE ? true : permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: importEnv.slug, secretPath: importPath @@ -672,7 +705,7 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, path); @@ -722,7 +755,7 @@ export const secretServiceFactory = ({ actor === ActorType.SERVICE ? true : permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: importEnv.slug, secretPath: importPath @@ -740,6 +773,7 @@ export const secretServiceFactory = ({ if (secretBlindIndex === importedSecrets[i].secrets[j].secretBlindIndex) { return { ...importedSecrets[i].secrets[j], + secretValueHidden: false, workspace: projectId, environment: importedSecrets[i].environment, secretPath: importedSecrets[i].secretPath @@ -750,7 +784,13 @@ export const secretServiceFactory = ({ } if (!secret) throw new NotFoundError({ message: `Secret with name '${secretName}' not found` }); - return { ...secret, workspace: projectId, environment, secretPath: path }; + return { + ...secret, + secretValueHidden: false, // Always false because we check permission at the beginning of the function + workspace: projectId, + environment, + secretPath: path + }; }; const createManySecret = async ({ @@ -772,7 +812,7 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -860,7 +900,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -902,8 +942,8 @@ export const secretServiceFactory = ({ if (tagIds.length !== tags.length) throw new NotFoundError({ message: "One or more tags not found" }); const references = await getSecretReference(projectId); - const secrets = await secretDAL.transaction(async (tx) => - fnSecretBulkUpdate({ + const secrets = await secretDAL.transaction(async (tx) => { + const updatedSecrets = await fnSecretBulkUpdate({ folderId, projectId, tx, @@ -933,8 +973,18 @@ export const secretServiceFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL - }) - ); + }); + + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + ); + + return updatedSecrets.map((secret) => ({ + ...secret, + ...conditionallyHideSecretValue(secretValueHidden, secret) + })); + }); await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ @@ -968,7 +1018,7 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -1020,7 +1070,15 @@ export const secretServiceFactory = ({ } } - return secrets; + const secretValueHidden = !permission.can( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + ); + + return secrets.map((secret) => ({ + ...secret, + ...conditionallyHideSecretValue(secretValueHidden, secret) + })); }); await snapshotService.performSnapshot(folderId); @@ -1181,6 +1239,7 @@ export const secretServiceFactory = ({ secretName, path: secretPath, environment, + viewSecretValue: false, type: "shared" }); @@ -1195,10 +1254,11 @@ export const secretServiceFactory = ({ | (typeof groupPermissions)[number] ) => { const allowedActions = [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Delete, - ProjectPermissionActions.Create, - ProjectPermissionActions.Edit + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Delete, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Edit ].filter((action) => entityPermission.permission.can( action, @@ -1235,11 +1295,13 @@ export const secretServiceFactory = ({ actorId, actorOrgId, actorAuthMethod, + viewSecretValue, environment, includeImports, expandSecretReferences, recursive, tagSlugs = [], + throwOnMissingReadValuePermission = true, ...paramsV2 }: TGetSecretsRawDTO) => { const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); @@ -1250,6 +1312,8 @@ export const secretServiceFactory = ({ actorId, actor, actorOrgId, + viewSecretValue, + throwOnMissingReadValuePermission, environment, path, recursive, @@ -1258,6 +1322,7 @@ export const secretServiceFactory = ({ tagSlugs, ...paramsV2 }); + return { secrets, imports }; } @@ -1286,14 +1351,20 @@ export const secretServiceFactory = ({ recursive }); - const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey)); + const decryptedSecrets = secrets.map((el) => decryptSecretRaw({ ...el, secretValueHidden: false }, botKey)); const filteredSecrets = tagSlugs.length ? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug)))) : decryptedSecrets; const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => { const decryptedImportSecrets = importedSecrets.map((sec) => decryptSecretRaw( - { ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath }, + { + ...sec, + environment: el.environment, + workspace: projectId, + secretPath: el.secretPath, + secretValueHidden: false + }, botKey ) ); @@ -1304,6 +1375,7 @@ export const secretServiceFactory = ({ const importedEntries = decryptedImportSecrets.reduce( ( accum: { + secretValueHidden: boolean; secretKey: string; secretPath: string; workspace: string; @@ -1347,6 +1419,7 @@ export const secretServiceFactory = ({ Object.keys(secretsGroupByPath).map((groupedPath) => Promise.allSettled( secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => { + if (decryptedSecret.secretValueHidden) return; const expandedSecretValue = await expandSecret({ value: decryptedSecret.secretValue, secretPath: groupedPath, @@ -1363,6 +1436,7 @@ export const secretServiceFactory = ({ processedImports.map((processedImport) => Promise.allSettled( processedImport.secrets.map(async (decryptedSecret, index) => { + if (decryptedSecret.secretValueHidden) return; const expandedSecretValue = await expandSecret({ value: decryptedSecret.secretValue, secretPath: path, @@ -1400,6 +1474,7 @@ export const secretServiceFactory = ({ path, actor, environment, + viewSecretValue, projectId: workspaceId, expandSecretReferences, projectSlug, @@ -1419,6 +1494,7 @@ export const secretServiceFactory = ({ includeImports, actorAuthMethod, path, + viewSecretValue, actorOrgId, actor, actorId, @@ -1449,6 +1525,7 @@ export const secretServiceFactory = ({ message: `Project bot for project with ID '${projectId}' not found. Please upgrade your project.`, name: "bot_not_found_error" }); + const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey); if (expandSecretReferences) { @@ -1467,7 +1544,10 @@ export const secretServiceFactory = ({ decryptedSecret.secretValue = expandedSecretValue || ""; } - return { secretMetadata: undefined, ...decryptedSecret }; + return { + secretMetadata: undefined, + ...decryptedSecret + }; }; const createSecretRaw = async ({ @@ -1618,7 +1698,16 @@ export const secretServiceFactory = ({ tags: tagIds }); - return { type: SecretProtectionType.Direct as const, secret: decryptSecretRaw(secret, botKey) }; + return { + type: SecretProtectionType.Direct as const, + secret: decryptSecretRaw( + { + ...secret, + secretValueHidden: false + }, + botKey + ) + }; }; const updateSecretRaw = async ({ @@ -2014,7 +2103,7 @@ export const secretServiceFactory = ({ return { type: SecretProtectionType.Direct as const, secrets: secrets.map((secret) => - decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey) + decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath, secretValueHidden: false }, botKey) ) }; }; @@ -2303,6 +2392,12 @@ export const secretServiceFactory = ({ const folder = await folderDAL.findById(secret.folderId); if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` }); + const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]); + + if (!folderWithPath) { + throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` }); + } + const { botKey } = await projectBotService.getBotKey(folder.projectId); if (!botKey) throw new NotFoundError({ message: `Project bot for project with ID '${folder.projectId}' not found` }); @@ -2316,18 +2411,42 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); - const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); - return secretVersions.map((el) => - decryptSecretRaw( + const secretVersions = await secretVersionDAL.findBySecretId(secretId, { + offset, + limit, + sort: [["createdAt", "desc"]] + }); + return secretVersions.map((el) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key: botKey + }); + + const secretValueHidden = permission.cannot( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment: folder.environment.envSlug, + secretPath: folderWithPath.path, + secretName: secretKey, + ...(el.tags?.length && { + secretTags: el.tags.map((tag) => tag.slug) + }) + }) + ); + + return decryptSecretRaw( { + secretValueHidden, ...el, workspace: folder.projectId, environment: folder.environment.envSlug, - secretPath: "/" + secretPath: folderWithPath.path }, botKey - ) - ); + ); + }); }; const attachTags = async ({ @@ -2353,7 +2472,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -2459,7 +2578,7 @@ export const secretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -2625,7 +2744,7 @@ export const secretServiceFactory = ({ message: `Project with slug '${projectSlug}' not found` }); } - if (project.version === 3) { + if (project.version === ProjectVersion.V3) { return secretV2BridgeService.moveSecrets({ sourceEnvironment, sourceSecretPath, @@ -2650,30 +2769,6 @@ export const secretServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { - environment: sourceEnvironment, - secretPath: sourceSecretPath - }) - ); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationSecretPath - }) - ); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationSecretPath - }) - ); - const { botKey } = await projectBotService.getBotKey(project.id); if (!botKey) { throw new NotFoundError({ @@ -2701,11 +2796,9 @@ export const secretServiceFactory = ({ }); } - const sourceSecrets = await secretDAL.find({ + const sourceSecrets = await secretDAL.findManySecretsWithTags({ type: SecretType.Shared, - $in: { - id: secretIds - } + secretIds }); if (sourceSecrets.length !== secretIds.length) { @@ -2714,21 +2807,52 @@ export const secretServiceFactory = ({ }); } - const decryptedSourceSecrets = sourceSecrets.map((secret) => ({ - ...secret, - secretKey: decryptSymmetric128BitHexKeyUTF8({ + const sourceActions = [ + ProjectPermissionSecretActions.Delete, + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue + ] as const; + const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const; + + const decryptedSourceSecrets = sourceSecrets.map((secret) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretKeyCiphertext, iv: secret.secretKeyIV, tag: secret.secretKeyTag, key: botKey - }), - secretValue: decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key: botKey - }) - })); + }); + + for (const destinationAction of destinationActions) { + ForbiddenError.from(permission).throwUnlessCan( + destinationAction, + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationSecretPath + }) + ); + } + + for (const sourceAction of sourceActions) { + ForbiddenError.from(permission).throwUnlessCan( + sourceAction, + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath + }) + ); + } + + return { + ...secret, + secretKey, + secretValue: decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key: botKey + }) + }; + }); let isSourceUpdated = false; let isDestinationUpdated = false; diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 46aedc2ee..c7b8a067d 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -184,6 +184,8 @@ export type TGetSecretsRawDTO = { expandSecretReferences?: boolean; path: string; environment: string; + viewSecretValue: boolean; + throwOnMissingReadValuePermission?: boolean; includeImports?: boolean; recursive?: boolean; tagSlugs?: string[]; @@ -209,6 +211,7 @@ export type TGetASecretRawDTO = { secretName: string; path: string; environment: string; + viewSecretValue: boolean; expandSecretReferences?: boolean; type: "shared" | "personal"; includeImports?: boolean; @@ -417,7 +420,7 @@ export type TCreateManySecretsRawFnFactory = { kmsService: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" + "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; @@ -454,7 +457,7 @@ export type TUpdateManySecretsRawFnFactory = { kmsService: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" + "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; diff --git a/backend/src/services/secret/secret-version-dal.ts b/backend/src/services/secret/secret-version-dal.ts index 8e77858a5..8e4544c19 100644 --- a/backend/src/services/secret/secret-version-dal.ts +++ b/backend/src/services/secret/secret-version-dal.ts @@ -1,9 +1,9 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas"; +import { SecretVersionsSchema, TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; import { QueueName } from "@app/queue"; @@ -12,6 +12,50 @@ export type TSecretVersionDALFactory = ReturnType { const secretVersionOrm = ormify(db, TableName.SecretVersion); + const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt = {}) => { + try { + const query = (tx || db.replicaNode())(TableName.SecretVersion) + .where(`${TableName.SecretVersion}.secretId`, secretId) + .leftJoin(TableName.Secret, `${TableName.SecretVersion}.secretId`, `${TableName.Secret}.id`) + .leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`) + .leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`) + .select(selectAllTableCols(TableName.SecretVersion)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); + } + + const docs = await query; + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretVersionsSchema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + + return data; + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.SecretVersion}: FindBySecretId` }); + } + }; + // This will fetch all latest secret versions from a folder const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { try { @@ -149,6 +193,7 @@ export const secretVersionDALFactory = (db: TDbClient) => { findLatestVersionMany, bulkUpdate, findLatestVersionByFolderId, + findBySecretId, bulkUpdateNoVersionIncrement }; }; diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index 654917feb..9b87c29f8 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -5,7 +5,11 @@ import bcrypt from "bcrypt"; import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; @@ -67,7 +71,7 @@ export const serviceTokenServiceFactory = ({ scopes.forEach(({ environment, secretPath }) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); }); diff --git a/backend/src/services/service-token/service-token-types.ts b/backend/src/services/service-token/service-token-types.ts index f1a700908..d06d90a9d 100644 --- a/backend/src/services/service-token/service-token-types.ts +++ b/backend/src/services/service-token/service-token-types.ts @@ -7,7 +7,7 @@ export type TCreateServiceTokenDTO = { iv: string; tag: string; expiresIn?: number | null; - permissions: ("read" | "write")[]; + permissions: ("read" | "write" | "readValue")[]; } & TProjectPermission; export type TGetServiceTokenInfoDTO = Omit; diff --git a/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx b/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx index 8164353d8..b8724f5dc 100644 --- a/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx +++ b/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx @@ -1,13 +1,21 @@ -import { useState } from "react"; -import { faChevronRight, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"; +/* eslint-disable no-nested-ternary */ +import { useEffect, useState } from "react"; +import { + faChevronRight, + faExclamationTriangle, + faEye, + faEyeSlash +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as Collapsible from "@radix-ui/react-collapsible"; +import { AxiosError } from "axios"; import { twMerge } from "tailwind-merge"; +import { createNotification } from "@app/components/notifications"; import { FormControl, FormLabel, SecretInput, Spinner, Tooltip } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { useGetSecretReferenceTree } from "@app/hooks/api"; -import { TSecretReferenceTraceNode } from "@app/hooks/api/types"; +import { ApiErrorTypes, TApiErrors, TSecretReferenceTraceNode } from "@app/hooks/api/types"; import style from "./SecretReferenceDetails.module.css"; @@ -84,7 +92,7 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; - const { data, isPending } = useGetSecretReferenceTree({ + const { data, isPending, isError, error } = useGetSecretReferenceTree({ secretPath, environmentSlug: environment, projectId, @@ -94,6 +102,26 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop const tree = data?.tree; const secretValue = data?.value; + useEffect(() => { + if (error instanceof AxiosError) { + const err = error?.response?.data as TApiErrors; + + if (err?.error === ApiErrorTypes.CustomForbiddenError) { + createNotification({ + title: "You don't have permission to view reference tree", + text: "You don't have permission to view one or more of the referenced secrets.", + type: "error" + }); + return; + } + createNotification({ + title: "Error fetching secret reference tree", + text: "Please try again later.", + type: "error" + }); + } + }, [error]); + if (isPending) { return (
@@ -114,11 +142,16 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
- {tree && ( + {isError ? ( +
+ +

Error fetching secret reference tree

+
+ ) : tree ? (
- )} + ) : null}
Click a secret key to view its sub-references. diff --git a/frontend/src/components/v2/Blur/Blur.tsx b/frontend/src/components/v2/Blur/Blur.tsx new file mode 100644 index 000000000..bd1ded40a --- /dev/null +++ b/frontend/src/components/v2/Blur/Blur.tsx @@ -0,0 +1,22 @@ +import { twMerge } from "tailwind-merge"; + +import { Tooltip } from "../Tooltip/Tooltip"; + +interface IProps { + className?: string; + tooltipText?: string; +} + +export const Blur = ({ className, tooltipText }: IProps) => { + return ( + +
+ ******** +
+
+ ); +}; diff --git a/frontend/src/components/v2/Blur/index.tsx b/frontend/src/components/v2/Blur/index.tsx new file mode 100644 index 000000000..50fb7ccc6 --- /dev/null +++ b/frontend/src/components/v2/Blur/index.tsx @@ -0,0 +1 @@ +export { Blur } from "./Blur"; diff --git a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx index 6fbe96238..dd6d3575e 100644 --- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx +++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx @@ -120,6 +120,7 @@ export const InfisicalSecretInput = forwardRef( const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused; const { data: secrets } = useGetProjectSecrets({ + viewSecretValue: false, environment: suggestionSource.environment || "", secretPath: suggestionSource.secretPath || "", workspaceId, diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index d368a949f..260ffce09 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 ProjectPermissionSecretActions { + DescribeSecret = "read", + ReadValue = "readValue", + Create = "create", + Edit = "edit", + Delete = "delete" +} + export enum ProjectPermissionDynamicSecretActions { ReadRootCredential = "read-root-credential", CreateRootCredential = "create-root-credential", @@ -138,7 +146,7 @@ export type SecretImportSubjectFields = { export type ProjectPermissionSet = | [ - ProjectPermissionActions, + ProjectPermissionSecretActions, ( | ProjectPermissionSub.Secrets | (ForcedSubject & SecretSubjectFields) diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx index 4b63be988..f704081a9 100644 --- a/frontend/src/hooks/api/dashboard/queries.tsx +++ b/frontend/src/hooks/api/dashboard/queries.tsx @@ -207,6 +207,7 @@ export const useGetProjectSecretsDetails = ( search = "", includeSecrets, includeFolders, + viewSecretValue, includeImports, includeDynamicSecrets, tags @@ -231,6 +232,7 @@ export const useGetProjectSecretsDetails = ( limit, orderBy, orderDirection, + viewSecretValue, offset, projectId, environment, @@ -247,6 +249,7 @@ export const useGetProjectSecretsDetails = ( limit, orderBy, orderDirection, + viewSecretValue, offset, projectId, environment, diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index 786b2b43a..4808b463c 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -69,6 +69,7 @@ export type TGetDashboardProjectSecretsDetailsDTO = Omit< TGetDashboardProjectSecretsOverviewDTO, "environments" > & { + viewSecretValue: boolean; environment: string; includeImports?: boolean; tags: Record; diff --git a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx index 74acf744b..39fa1058a 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx +++ b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx @@ -79,6 +79,7 @@ export const decryptSecrets = ( id: encSecret.id, env: encSecret.environment, key: secretKey, + secretValueHidden: encSecret.secretValueHidden, value: secretValue, tags: encSecret.tags, comment: secretComment, diff --git a/frontend/src/hooks/api/secretImports/queries.tsx b/frontend/src/hooks/api/secretImports/queries.tsx index c2ddd2fbe..dfc8c6497 100644 --- a/frontend/src/hooks/api/secretImports/queries.tsx +++ b/frontend/src/hooks/api/secretImports/queries.tsx @@ -137,6 +137,7 @@ export const useGetImportedSecretsSingleEnv = ({ env: encSecret.environment, key: encSecret.secretKey, value: encSecret.secretValue, + secretValueHidden: encSecret.secretValueHidden, tags: encSecret.tags, comment: encSecret.secretComment, createdAt: encSecret.createdAt, @@ -176,6 +177,7 @@ export const useGetImportedSecretsAllEnvs = ({ env: encSecret.environment, key: encSecret.secretKey, value: encSecret.secretValue, + secretValueHidden: encSecret.secretValueHidden, tags: encSecret.tags, comment: encSecret.secretComment, createdAt: encSecret.createdAt, diff --git a/frontend/src/hooks/api/secretSnapshots/queries.tsx b/frontend/src/hooks/api/secretSnapshots/queries.tsx index 8a16b4180..d273bd5ea 100644 --- a/frontend/src/hooks/api/secretSnapshots/queries.tsx +++ b/frontend/src/hooks/api/secretSnapshots/queries.tsx @@ -75,6 +75,7 @@ export const useGetSnapshotSecrets = ({ snapshotId }: TSnapshotDataProps) => id: secretVersion.secretId, env: data.environment.slug, key: secretVersion.secretKey, + secretValueHidden: secretVersion.secretValueHidden, value: secretVersion.secretValue || "", tags: secretVersion.tags, comment: secretVersion.secretComment, diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index a6dd959b0..3e82c3b90 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -26,8 +26,13 @@ import { export const secretKeys = { // this is also used in secretSnapshot part - getProjectSecret: ({ workspaceId, environment, secretPath }: TGetProjectSecretsKey) => - [{ workspaceId, environment, secretPath }, "secrets"] as const, + getProjectSecret: ({ + workspaceId, + environment, + secretPath, + viewSecretValue + }: TGetProjectSecretsKey) => + [{ workspaceId, environment, secretPath, viewSecretValue }, "secrets"] as const, getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const, getSecretAccessList: ({ workspaceId, @@ -44,13 +49,15 @@ export const fetchProjectSecrets = async ({ environment, secretPath, includeImports, - expandSecretReferences + expandSecretReferences, + viewSecretValue }: TGetProjectSecretsKey) => { const { data } = await apiRequest.get("/api/v3/secrets/raw", { params: { environment, workspaceId, secretPath, + viewSecretValue, expandSecretReferences, include_imports: includeImports } @@ -68,6 +75,7 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => { env: el.environment, key: el.secretKey, value: el.secretValue, + secretValueHidden: el.secretValueHidden, tags: el.tags || [], comment: el.secretComment || "", reminderRepeatDays: el.secretReminderRepeatDays, @@ -107,6 +115,7 @@ export const useGetProjectSecrets = ({ workspaceId, environment, secretPath, + viewSecretValue, options }: TGetProjectSecretsDTO & { options?: Omit< @@ -123,8 +132,13 @@ export const useGetProjectSecrets = ({ ...options, // wait for all values to be available enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true), - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }), - queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }), + queryKey: secretKeys.getProjectSecret({ + workspaceId, + environment, + secretPath, + viewSecretValue + }), + queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath, viewSecretValue }), select: useCallback( (data: Awaited>) => mergePersonalSecrets(data.secrets), [] diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 9e0a82dda..5c4d4ffc6 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -19,6 +19,7 @@ export type EncryptedSecret = { secretValueCiphertext: string; secretValueIV: string; secretValueTag: string; + secretValueHidden: boolean; __v: number; createdAt: string; updatedAt: string; @@ -37,6 +38,7 @@ export type SecretV3RawSanitized = { version: number; key: string; value?: string; + secretValueHidden: boolean; comment?: string; reminderRepeatDays?: number | null; reminderNote?: string | null; @@ -61,6 +63,7 @@ export type SecretV3Raw = { environment: string; version: number; type: string; + secretValueHidden: boolean; secretKey: string; secretPath: string; secretValue?: string; @@ -95,6 +98,7 @@ export type SecretVersions = { envId: string; secretKey: string; secretValue?: string; + secretValueHidden: boolean; secretComment?: string; tags: WsTag[]; __v: number; @@ -115,6 +119,7 @@ export type TGetProjectSecretsKey = { environment: string; secretPath?: string; includeImports?: boolean; + viewSecretValue?: boolean; expandSecretReferences?: boolean; }; diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index cda34ad60..8f2acbbd7 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -47,7 +47,8 @@ export enum ApiErrorTypes { PermissionBoundaryError = "PermissionBoundaryError", BadRequestError = "BadRequest", UnauthorizedError = "UnauthorizedError", - ForbiddenError = "PermissionDenied" + ForbiddenError = "PermissionDenied", + CustomForbiddenError = "ForbiddenError" } export type TApiErrors = @@ -70,6 +71,12 @@ export type TApiErrors = details: PureAbility["rules"]; statusCode: 403; } + | { + reqId: string; + error: ApiErrorTypes.CustomForbiddenError; + message: string; + statusCode: 403; + } | { reqId: string; statusCode: 400; diff --git a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx index 46a9c5680..a669a8360 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx @@ -58,6 +58,7 @@ const schema = z.object({ permissions: z .object({ read: z.boolean(), + readValue: z.boolean(), write: z.boolean() }) .required() @@ -296,14 +297,19 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => { name="permissions" defaultValue={{ read: true, + readValue: false, write: false }} render={({ field: { onChange, value }, fieldState: { error } }) => { const options = [ { - label: "Read (default)", + label: "Describe Secret (default)", value: "read" }, + { + label: "Read Value (optional)", + value: "readValue" + }, { label: "Write (optional)", value: "write" diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx index de2231fc9..8aca40a34 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx @@ -117,24 +117,27 @@ export const GeneralPermissionPolicies = {actions.map(({ label, value }) => { if (typeof value !== "string") return undefined; + return ( ( -
- - {label} - -
- )} + render={({ field }) => { + return ( +
+ + {label} + +
+ ); + }} /> ); })} diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index 7dcb118fd..e26ec8bc4 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -9,6 +9,7 @@ import { PermissionConditionOperators, ProjectPermissionDynamicSecretActions, ProjectPermissionKmipActions, + ProjectPermissionSecretActions, ProjectPermissionSecretSyncActions, TPermissionCondition, TPermissionConditionOperators @@ -22,6 +23,14 @@ const GeneralPolicyActionSchema = z.object({ create: z.boolean().optional() }); +const SecretPolicyActionSchema = z.object({ + read: z.boolean().optional(), // describe secret + edit: z.boolean().optional(), + delete: z.boolean().optional(), + create: z.boolean().optional(), + readValue: z.boolean().optional() +}); + const CmekPolicyActionSchema = z.object({ read: z.boolean().optional(), edit: z.boolean().optional(), @@ -114,7 +123,7 @@ export const projectRoleFormSchema = z.object({ .refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }), permissions: z .object({ - [ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({ + [ProjectPermissionSub.Secrets]: SecretPolicyActionSchema.extend({ inverted: z.boolean().optional(), conditions: ConditionSchema }) @@ -283,6 +292,28 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { }); return; } + + if (subject === ProjectPermissionSub.Secrets) { + const canRead = action.includes(ProjectPermissionSecretActions.DescribeSecret); + const canEdit = action.includes(ProjectPermissionSecretActions.Edit); + const canDelete = action.includes(ProjectPermissionSecretActions.Delete); + const canCreate = action.includes(ProjectPermissionSecretActions.Create); + const canReadValue = action.includes(ProjectPermissionSecretActions.ReadValue); + + // from above statement we are sure it won't be undefined + formVal[subject]!.push({ + read: canRead, + create: canCreate, + edit: canEdit, + delete: canDelete, + readValue: canReadValue, + conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], + inverted + }); + + return; + } + // for other subjects const canRead = action.includes(ProjectPermissionActions.Read); const canEdit = action.includes(ProjectPermissionActions.Edit); @@ -483,8 +514,9 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { [ProjectPermissionSub.Secrets]: { title: "Secrets", actions: [ - { label: "Read", value: "read" }, + { label: "Describe Secret", value: "read" }, { label: "Create", value: "create" }, + { label: "Read Value", value: "readValue" }, { label: "Modify", value: "edit" }, { label: "Remove", value: "delete" } ] diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx index c894d6b14..4847feb69 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -19,6 +19,7 @@ import { import { getKeyValue } from "@app/helpers/parseEnvVar"; import { useCreateFolder, useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api"; import { SecretType } from "@app/hooks/api/types"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; const typeSchema = z .object({ @@ -275,7 +276,7 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => { isMulti options={environments.filter((environment) => permission.can( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment: environment.slug, secretPath, diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx index d3dfd49e7..4887d4fd7 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -25,6 +25,7 @@ import { ModalTrigger, Tooltip } from "@app/components/v2"; +import { Blur } from "@app/components/v2/Blur"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { useToggle } from "@app/hooks"; @@ -39,6 +40,7 @@ type Props = { isVisible?: boolean; isImportedSecret: boolean; environment: string; + secretValueHidden: boolean; secretPath: string; onSecretCreate: (env: string, key: string, value: string) => Promise; onSecretUpdate: ( @@ -58,6 +60,7 @@ export const SecretEditRow = ({ isImportedSecret, onSecretUpdate, secretName, + secretValueHidden, onSecretCreate, onSecretDelete, environment, @@ -140,24 +143,29 @@ export const SecretEditRow = ({ />
- ( - - )} - /> + {secretValueHidden ? ( + + ) : ( + ( + + )} + /> + )}
+
-
NO ACCESS
+
diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx index b9376f6b4..3a66f6896 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx @@ -221,10 +221,13 @@ export const SecretOverviewTableRow = ({ secretPath={secretPath} isVisible={isSecretVisible} secretName={secretKey} + secretValueHidden={secret?.secretValueHidden || false} defaultValue={ - secret?.valueOverride || - secret?.value || - importedSecret?.secret?.value + secret?.secretValueHidden + ? "" + : secret?.valueOverride || + secret?.value || + importedSecret?.secret?.value } secretId={secret?.id} isOverride={Boolean(secret?.valueOverride)} diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx index ae1916b86..3b05f10fa 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx @@ -10,12 +10,8 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { IconButton, Input, Spinner, Tooltip } from "@app/components/v2"; -import { - ProjectPermissionActions, - ProjectPermissionSub, - useProjectPermission, - useWorkspace -} from "@app/context"; +import { ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { useToggle } from "@app/hooks"; import { useUpdateSecretV3 } from "@app/hooks/api"; import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types"; @@ -55,8 +51,8 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath } secretTags: (secretDetails?.tags || []).map((i) => i.slug) }); const isSecretInEnvReadOnly = - permission.can(ProjectPermissionActions.Read, secretPermissionSubject) && - permission.cannot(ProjectPermissionActions.Edit, secretPermissionSubject); + permission.can(ProjectPermissionSecretActions.DescribeSecret, secretPermissionSubject) && + permission.cannot(ProjectPermissionSecretActions.Edit, secretPermissionSubject); if (isSecretInEnvReadOnly) { return true; } diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx index e901a4ff7..9943de5b6 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx @@ -110,21 +110,31 @@ export const QuickSearchSecretItem = ({ )} {isSingleEnv ? ( - { - e.stopPropagation(); - const el = envSlugMap.get(groupSecret.env)?.name; - if (el) { - handleCopy(groupSecret.value!, el); - } - }} + - - + { + e.stopPropagation(); + const el = envSlugMap.get(groupSecret.env)?.name; + if (el) { + handleCopy(groupSecret.value!, el); + } + }} + > + + + ) : ( @@ -158,14 +168,24 @@ export const QuickSearchSecretItem = ({ )} - - - + + + + Hover to Reveal... diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx index 4738e411e..4e048b941 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx @@ -11,6 +11,7 @@ import { useProjectPermission, useWorkspace } from "@app/context"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { usePopUp } from "@app/hooks"; import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api"; import { @@ -58,7 +59,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri // user should have the ability to delete secrets/folders in at least one of the envs const shouldShowDelete = userAvailableEnvs.some((env) => permission.can( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath, @@ -110,7 +111,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri (accum: TDeleteSecretBatchDTO["secrets"], secretRecord) => { const entry = secretRecord[env.slug]; const canDeleteSecret = permission.can( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath, diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx index 8a27a24c0..a81fa3e53 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/components/MoveSecretsDialog/MoveSecretsDialog.tsx @@ -25,7 +25,8 @@ import { Spinner, Switch } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { useDebounce } from "@app/hooks"; import { useMoveSecrets } from "@app/hooks/api"; import { useGetProjectSecretsQuickSearch } from "@app/hooks/api/dashboard"; @@ -95,7 +96,7 @@ const Content = ({ env.slug, { missingPermissions: permission.cannot( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: sourceSecretPath, diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index daf875a78..5cbefe9c3 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -25,6 +25,7 @@ import { useProjectPermission, useWorkspace } from "@app/context"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { useGetImportedSecretsSingleEnv, @@ -103,7 +104,16 @@ const Page = () => { const projectSlug = currentWorkspace?.slug || ""; const secretPath = (routerQueryParams.secretPath as string) || "/"; const canReadSecret = permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: "*", + secretTags: ["*"] + }) + ); + const canReadSecretValue = permission.can( + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -176,6 +186,7 @@ const Page = () => { orderDirection, includeImports: canReadSecretImports && filter.include.import, includeFolders: filter.include.folder, + viewSecretValue: canReadSecretValue, includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic, includeSecrets: canReadSecret && filter.include.secret, tags: filter.tags diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx index 03ff3dd57..d545ba748 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx @@ -21,6 +21,7 @@ import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { AxiosError } from "axios"; import FileSaver from "file-saver"; import { twMerge } from "tailwind-merge"; @@ -54,7 +55,7 @@ import { import { usePopUp } from "@app/hooks"; import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api"; import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries"; -import { SecretType, WsTag } from "@app/hooks/api/types"; +import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types"; import { SecretSearchInput } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput"; import { @@ -152,51 +153,71 @@ export const ActionBar = ({ }; const handleSecretDownload = async () => { - const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({ - workspaceId, - expandSecretReferences: true, - includeImports: true, - environment, - secretPath - }); - const secretsPicked = new Set(); - const secretsToDownload: { key: string; value?: string; comment?: string }[] = []; - localSecrets.forEach((el) => { - secretsPicked.add(el.secretKey); - secretsToDownload.push({ - key: el.secretKey, - value: el.secretValue, - comment: el.secretComment + try { + const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({ + workspaceId, + expandSecretReferences: true, + includeImports: true, + environment, + secretPath + }); + const secretsPicked = new Set(); + const secretsToDownload: { key: string; value?: string; comment?: string }[] = []; + localSecrets.forEach((el) => { + secretsPicked.add(el.secretKey); + secretsToDownload.push({ + key: el.secretKey, + value: el.secretValue, + comment: el.secretComment + }); }); - }); - for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) { - for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) { - const secret = localImportedSecrets[i].secrets[j]; - if (!secretsPicked.has(secret.secretKey)) { - secretsToDownload.push({ - key: secret.secretKey, - value: secret.secretValue, - comment: secret.secretComment - }); + for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) { + for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) { + const secret = localImportedSecrets[i].secrets[j]; + if (!secretsPicked.has(secret.secretKey)) { + secretsToDownload.push({ + key: secret.secretKey, + value: secret.secretValue, + comment: secret.secretComment + }); + } + secretsPicked.add(secret.secretKey); } - secretsPicked.add(secret.secretKey); } + + const file = secretsToDownload + .sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase())) + .reduce( + (prev, { key, comment, value }, index) => + prev + + (comment + ? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n` + : `${key}=${value}\n`), + "" + ); + + const blob = new Blob([file], { type: "text/plain;charset=utf-8" }); + FileSaver.saveAs(blob, `${environment}.env`); + } catch (err) { + if (err instanceof AxiosError) { + const error = err?.response?.data as TApiErrors; + + if (error?.error === ApiErrorTypes.ForbiddenError && error.message.includes("readValue")) { + createNotification({ + title: "You don't have permission to download secrets", + text: "You don't have permission to view one or more of the secrets in the current folder. Please contact your administrator.", + type: "error" + }); + return; + } + } + createNotification({ + title: "Failed to download secrets", + text: "Please try again later.", + type: "error" + }); } - - const file = secretsToDownload - .sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase())) - .reduce( - (prev, { key, comment, value }, index) => - prev + - (comment - ? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n` - : `${key}=${value}\n`), - "" - ); - - const blob = new Blob([file], { type: "text/plain;charset=utf-8" }); - FileSaver.saveAs(blob, `${environment}.env`); }; const handleSecretBulkDelete = async () => { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index f557c53b3..d6da791bb 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -13,12 +13,14 @@ import { faShare, faTag, faTrash, - faUser + faUser, + faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { Link, useNavigate } from "@tanstack/react-router"; import { format } from "date-fns"; +import { twMerge } from "tailwind-merge"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; @@ -48,6 +50,7 @@ import { useProjectPermission, useWorkspace } from "@app/context"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { usePopUp, useToggle } from "@app/hooks"; import { useGetSecretVersion } from "@app/hooks/api"; import { ActorType } from "@app/hooks/api/auditLogs/enums"; @@ -128,7 +131,7 @@ export const SecretDetailSidebar = ({ const navigate = useNavigate(); const cannotEditSecret = permission.cannot( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -136,16 +139,29 @@ export const SecretDetailSidebar = ({ secretTags: selectTagSlugs }) ); + + const cannotReadSecretValue = permission.cannot( + ProjectPermissionSecretActions.ReadValue, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: secretKey, + secretTags: selectTagSlugs + }) + ); + const isReadOnly = permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: secretKey, secretTags: selectTagSlugs }) - ) && cannotEditSecret; + ) && + cannotEditSecret && + cannotReadSecretValue; const overrideAction = watch("overrideAction"); const isOverridden = @@ -325,38 +341,63 @@ export const SecretDetailSidebar = ({ key="secret-value" control={control} render={({ field }) => ( - - - +
+ + + + The value of this secret is hidden because you do not have the + read secret value permission. + +
+ ) : undefined + } + label="Value" + > +
+ + + + +
+ + )} /> )} -
- -
@@ -683,14 +724,17 @@ export const SecretDetailSidebar = ({
Version History
- {secretVersion?.map(({ createdAt, secretValue, version, id, actor }) => ( -
-
-
-
-
- v{version} + {secretVersion?.map( + ({ createdAt, secretValue, version, id, secretValueHidden, actor }, index) => ( +
+
+
+
+
+ v{version} +
+
{format(new Date(createdAt), "Pp")}
{format(new Date(createdAt), "Pp")}
@@ -734,6 +778,8 @@ export const SecretDetailSidebar = ({ type="button" className="select-none text-left" onClick={(e) => { + if (secretValueHidden) return; + navigator.clipboard.writeText(secretValue || ""); const target = e.currentTarget; target.style.borderBottom = "1px dashed"; @@ -753,6 +799,8 @@ export const SecretDetailSidebar = ({ }, 3000); }} onKeyDown={(e) => { + if (secretValueHidden) return; + if (e.key === "Enter" || e.key === " ") { navigator.clipboard.writeText(secretValue || ""); const target = e.currentTarget; @@ -774,7 +822,19 @@ export const SecretDetailSidebar = ({ } }} > - {secretValue} + + + {secretValueHidden ? "Hidden" : secretValue} + +
- {secretValue?.replace(/./g, "*")} + {secretValueHidden ? "******" : secretValue?.replace(/./g, "*")}
+
+ + setValue("value", secretValue)} + > + + + +
-
- - setValue("value", secretValue)} - > - - - -
-
- ))} + ) + )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx index 86905f351..02f7a0aa9 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-nested-ternary */ /* eslint-disable simple-import-sort/imports */ import { ProjectPermissionCan } from "@app/components/permissions"; import { @@ -44,6 +45,8 @@ import { SecretReferenceTree } from "@app/components/secrets/SecretReferenceDetails"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; +import { Blur } from "@app/components/v2/Blur"; import { FontAwesomeSpriteName, formSchema, @@ -99,8 +102,14 @@ export const SecretItem = memo( trigger, formState: { isDirty, isSubmitting, errors } } = useForm({ - defaultValues: secret, - values: secret, + defaultValues: { + ...secret, + value: secret.secretValueHidden ? "" : secret.value + }, + values: { + ...secret, + value: secret.secretValueHidden ? "" : secret.value + }, resolver: zodResolver(formSchema) }); @@ -123,7 +132,7 @@ export const SecretItem = memo( const isReadOnly = permission.can( - ProjectPermissionActions.Read, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -132,7 +141,7 @@ export const SecretItem = memo( }) ) && permission.cannot( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -141,6 +150,8 @@ export const SecretItem = memo( }) ); + const { secretValueHidden } = secret; + const [isSecValueCopied, setIsSecValueCopied] = useToggle(false); useEffect(() => { let timer: NodeJS.Timeout; @@ -272,6 +283,8 @@ export const SecretItem = memo( /> )} /> + ) : secretValueHidden ? ( + ) : ( )} @@ -293,6 +307,7 @@ export const SecretItem = memo(
{ className="w-full px-0 blur-sm placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent" />
-
- ******** -
+
))} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SecretItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SecretItem.tsx index d6680bcef..076cf3b6d 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SecretItem.tsx @@ -20,6 +20,7 @@ import { Tooltip, Tr } from "@app/components/v2"; +import { Blur } from "@app/components/v2/Blur"; import { useToggle } from "@app/hooks"; import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; @@ -120,11 +121,25 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => { Value {isModified && ( - + {preSecret?.secretValueHidden ? ( + + ) : ( + + )} )} - + {postSecret?.secretValueHidden ? ( + + ) : ( + + )} {Boolean(preSecret?.idOverride || postSecret?.idOverride) && ( From d74197aeb4b90423355a25fc6488b1b01f960fdb Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 5 Mar 2025 17:53:07 -0500 Subject: [PATCH 02/15] Revert "use forked pion turn server" This reverts commit bd66411d754df79fb22a0b333ea5205e90affef4. --- cli/go.sum | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cli/go.sum b/cli/go.sum index ab46476d7..d209fd1ca 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -367,6 +367,8 @@ github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= +github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -425,8 +427,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= From f1ba64aa66d2e453bd35ecf4dd136fa26d5e5953 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 8 Mar 2025 03:40:51 +0400 Subject: [PATCH 03/15] fix(view-secret-value): backwards compatibility for `read` --- ...kfill-secret-permissions-with-readvalue.ts | 313 -------------- .../src/ee/routes/v2/project-role-router.ts | 6 +- .../ee/services/permission/permission-fns.ts | 103 ++++- .../services/permission/project-permission.ts | 17 +- .../secret-approval-request-service.ts | 10 +- .../secret-snapshot-service.ts | 51 +-- .../server/routes/v2/service-token-router.ts | 2 +- .../integration/integration-service.ts | 25 +- .../src/services/project/project-service.ts | 6 +- .../secret-import/secret-import-service.ts | 63 ++- .../secret-sync/secret-sync-service.ts | 25 +- .../secret-v2-bridge-service.ts | 409 ++++++++---------- backend/src/services/secret/secret-fns.ts | 15 +- backend/src/services/secret/secret-service.ts | 134 +++--- .../service-token/service-token-types.ts | 2 +- .../context/ProjectPermissionContext/types.ts | 3 +- frontend/src/lib/fn/permission.ts | 36 ++ .../AddServiceTokenModal.tsx | 6 +- .../components/GeneralPermissionPolicies.tsx | 236 +++++----- .../ProjectRoleModifySection.utils.tsx | 64 ++- .../SecretRenameRow.tsx | 8 +- .../SecretDashboardPage.tsx | 17 +- .../SecretListView/SecretDetailSidebar.tsx | 23 +- .../components/SecretListView/SecretItem.tsx | 16 +- 24 files changed, 708 insertions(+), 882 deletions(-) delete mode 100644 backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts create mode 100644 frontend/src/lib/fn/permission.ts diff --git a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts b/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts deleted file mode 100644 index 9e567cefe..000000000 --- a/backend/src/db/migrations/20250218020306_backfill-secret-permissions-with-readvalue.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { MongoAbility, RawRuleOf } from "@casl/ability"; -import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; -import { Knex } from "knex"; -import { z } from "zod"; - -import { selectAllTableCols } from "@app/lib/knex"; - -import { TableName } from "../schemas"; - -enum ProjectPermissionSub { - Secrets = "secrets" -} - -enum SecretActions { - Read = "read", - ReadValue = "readValue" -} - -const UnpackedPermissionSchema = z.object({ - subject: z - .union([z.string().min(1), z.string().array()]) - .transform((el) => (typeof el !== "string" ? el[0] : el)) - .optional(), - action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)), - conditions: z.unknown().optional(), - inverted: z.boolean().optional() -}); - -const $unpackPermissions = (permissions: unknown) => - UnpackedPermissionSchema.array().parse(unpackRules((permissions || []) as PackRule>[])); - -const $updatePermissionsUp = (permissions: unknown) => { - const parsedPermissions = $unpackPermissions(permissions); - let shouldUpdate = false; - - for (let i = 0; i < parsedPermissions.length; i += 1) { - const parsedPermission = parsedPermissions[i]; - const { subject, action } = parsedPermission; - - if (subject === ProjectPermissionSub.Secrets) { - if (action.includes(SecretActions.Read) && !action.includes(SecretActions.ReadValue)) { - action.push(SecretActions.ReadValue); - parsedPermissions[i] = { ...parsedPermission, action }; - shouldUpdate = true; - } - } - } - - return { - parsedPermissions, - shouldUpdate - }; -}; - -const $updatePermissionsDown = (permissions: unknown) => { - const parsedPermissions = $unpackPermissions(permissions); - - let shouldUpdate = false; - for (let i = 0; i < parsedPermissions.length; i += 1) { - const parsedPermission = parsedPermissions[i]; - - const { subject, action } = parsedPermission; - - if (subject === ProjectPermissionSub.Secrets) { - const readValueIndex = action.indexOf(SecretActions.ReadValue); - - if (action.includes(SecretActions.ReadValue) && readValueIndex !== -1) { - action.splice(readValueIndex, 1); - parsedPermissions[i] = { ...parsedPermission, action }; - - shouldUpdate = true; - } - } - } - - const repackedPermissions = packRules(parsedPermissions); - - return { - repackedPermissions, - shouldUpdate - }; -}; - -const CHUNK_SIZE = 1000; - -export async function up(knex: Knex): Promise { - const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles)); - const projectIdentityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select( - selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege) - ); - const projectUserAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select( - selectAllTableCols(TableName.ProjectUserAdditionalPrivilege) - ); - - const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken)); - - const updatedServiceTokens = serviceTokens.reduce((acc, serviceToken) => { - const { permissions } = serviceToken; // Service tokens are special, and include an array of actions only. - - if (permissions.includes(SecretActions.Read) && !permissions.includes(SecretActions.ReadValue)) { - permissions.push(SecretActions.ReadValue); - acc.push({ - ...serviceToken, - permissions - }); - } - return acc; - }, []); - - if (updatedServiceTokens.length > 0) { - for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) { - const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ServiceToken) - .whereIn( - "id", - chunk.map((t) => t.id) - ) - .update({ - // @ts-expect-error -- raw query - permissions: knex.raw( - `CASE id - ${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")} - END`, - chunk.map((t) => t.permissions) - ) - }); - } - } - - const updatedRoles = projectRoles.reduce((acc, projectRole) => { - const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(projectRole.permissions); - - if (shouldUpdate) { - acc.push({ - ...projectRole, - permissions: JSON.stringify(packRules(parsedPermissions)) - }); - } - return acc; - }, []); - - const updatedIdentityAdditionalPrivileges = projectIdentityAdditionalPrivileges.reduce< - typeof projectIdentityAdditionalPrivileges - >((acc, identityAdditionalPrivilege) => { - const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(identityAdditionalPrivilege.permissions); - - if (shouldUpdate) { - acc.push({ - ...identityAdditionalPrivilege, - permissions: JSON.stringify(packRules(parsedPermissions)) - }); - } - return acc; - }, []); - - const updatedUserAdditionalPrivileges = projectUserAdditionalPrivileges.reduce< - typeof projectUserAdditionalPrivileges - >((acc, userAdditionalPrivilege) => { - const { shouldUpdate, parsedPermissions } = $updatePermissionsUp(userAdditionalPrivilege.permissions); - - if (shouldUpdate) { - acc.push({ - ...userAdditionalPrivilege, - permissions: JSON.stringify(packRules(parsedPermissions)) - }); - } - return acc; - }, []); - - if (updatedRoles.length > 0) { - for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) { - const chunk = updatedRoles.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]); - } - } - - if (updatedIdentityAdditionalPrivileges.length > 0) { - for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) { - const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); - } - } - - if (updatedUserAdditionalPrivileges.length > 0) { - for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) { - const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); - } - } -} - -export async function down(knex: Knex): Promise { - const projectRoles = await knex(TableName.ProjectRoles).select(selectAllTableCols(TableName.ProjectRoles)); - const identityAdditionalPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select( - selectAllTableCols(TableName.IdentityProjectAdditionalPrivilege) - ); - const userAdditionalPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select( - selectAllTableCols(TableName.ProjectUserAdditionalPrivilege) - ); - const serviceTokens = await knex(TableName.ServiceToken).select(selectAllTableCols(TableName.ServiceToken)); - - const updatedServiceTokens = serviceTokens.reduce((acc, serviceToken) => { - const { permissions } = serviceToken; - - if (permissions.includes(SecretActions.ReadValue)) { - permissions.splice(permissions.indexOf(SecretActions.ReadValue), 1); - acc.push({ - ...serviceToken, - permissions - }); - } - return acc; - }, []); - - if (updatedServiceTokens.length > 0) { - for (let i = 0; i < updatedServiceTokens.length; i += CHUNK_SIZE) { - const chunk = updatedServiceTokens.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ServiceToken) - .whereIn( - "id", - chunk.map((t) => t.id) - ) - .update({ - // @ts-expect-error -- raw query - permissions: knex.raw( - `CASE id - ${chunk.map((t) => `WHEN '${t.id}' THEN ?::text[]`).join(" ")} - END`, - chunk.map((t) => t.permissions) - ) - }); - } - } - - const updatedRoles = projectRoles.reduce((acc, projectRole) => { - const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(projectRole.permissions); - - if (shouldUpdate) { - acc.push({ - ...projectRole, - permissions: JSON.stringify(repackedPermissions) - }); - } - return acc; - }, []); - - const updatedIdentityAdditionalPrivileges = identityAdditionalPrivileges.reduce( - (acc, identityAdditionalPrivilege) => { - const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(identityAdditionalPrivilege.permissions); - - if (shouldUpdate) { - acc.push({ - ...identityAdditionalPrivilege, - permissions: JSON.stringify(repackedPermissions) - }); - } - return acc; - }, - [] - ); - - const updatedUserAdditionalPrivileges = userAdditionalPrivileges.reduce( - (acc, userAdditionalPrivilege) => { - const { shouldUpdate, repackedPermissions } = $updatePermissionsDown(userAdditionalPrivilege.permissions); - - if (shouldUpdate) { - acc.push({ - ...userAdditionalPrivilege, - permissions: JSON.stringify(repackedPermissions) - }); - } - return acc; - }, - [] - ); - - if (updatedRoles.length > 0) { - for (let i = 0; i < updatedRoles.length; i += CHUNK_SIZE) { - const chunk = updatedRoles.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(["permissions"]); - } - } - - if (updatedIdentityAdditionalPrivileges.length > 0) { - for (let i = 0; i < updatedIdentityAdditionalPrivileges.length; i += CHUNK_SIZE) { - const chunk = updatedIdentityAdditionalPrivileges.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); - } - } - - if (updatedUserAdditionalPrivileges.length > 0) { - for (let i = 0; i < updatedUserAdditionalPrivileges.length; i += CHUNK_SIZE) { - const chunk = updatedUserAdditionalPrivileges.slice(i, i + CHUNK_SIZE); - - // eslint-disable-next-line no-await-in-loop - await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(["permissions"]); - } - } -} diff --git a/backend/src/ee/routes/v2/project-role-router.ts b/backend/src/ee/routes/v2/project-role-router.ts index 2d3b1984d..c3b3c11f2 100644 --- a/backend/src/ee/routes/v2/project-role-router.ts +++ b/backend/src/ee/routes/v2/project-role-router.ts @@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra"; import { z } from "zod"; import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; +import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { PROJECT_ROLE } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -92,7 +93,10 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { .describe(PROJECT_ROLE.UPDATE.slug), name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description), - permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() + permissions: ProjectPermissionV2Schema.array() + .describe(PROJECT_ROLE.UPDATE.permissions) + .optional() + .superRefine(checkForInvalidPermissionCombination) }), response: { 200: z.object({ diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index 80a58db0a..97113a562 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -1,7 +1,108 @@ +/* eslint-disable no-nested-ternary */ +import { ForbiddenError, MongoAbility, subject } from "@casl/ability"; +import { z } from "zod"; + import { TOrganizations } from "@app/db/schemas"; -import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; +import { + ProjectPermissionSecretActions, + ProjectPermissionSet, + ProjectPermissionSub, + ProjectPermissionV2Schema, + SecretSubjectFields +} from "./project-permission"; + +export function CheckForbiddenErrorSecretsSubject( + permission: MongoAbility, + action: Extract< + ProjectPermissionSecretActions, + ProjectPermissionSecretActions.ReadValue | ProjectPermissionSecretActions.DescribeSecret + >, + subjectFields?: SecretSubjectFields +) { + try { + if (subjectFields) { + ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields)); + } else { + ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets); + } + } catch { + if (subjectFields) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeAndReadValue, + subject(ProjectPermissionSub.Secrets, subjectFields) + ); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeAndReadValue, + ProjectPermissionSub.Secrets + ); + } + } +} + +export function CheckCanSecretsSubject( + permission: MongoAbility, + action: Extract< + ProjectPermissionSecretActions, + ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue + >, + subjectFields?: SecretSubjectFields +) { + let canNewPermission = false; + let canOldPermission = false; + + if (subjectFields) { + canNewPermission = permission.can(action, subject(ProjectPermissionSub.Secrets, subjectFields)); + canOldPermission = permission.can( + ProjectPermissionSecretActions.DescribeAndReadValue, + subject(ProjectPermissionSub.Secrets, subjectFields) + ); + } else { + canNewPermission = permission.can(action, ProjectPermissionSub.Secrets); + canOldPermission = permission.can( + ProjectPermissionSecretActions.DescribeAndReadValue, + ProjectPermissionSub.Secrets + ); + } + + return canNewPermission || canOldPermission; +} + +const OptionalArrayPermissionSchema = ProjectPermissionV2Schema.array().optional(); +export function checkForInvalidPermissionCombination(permissions: z.infer) { + if (!permissions) return; + + for (const permission of permissions) { + if (permission.subject === ProjectPermissionSub.Secrets) { + if (permission.action.includes(ProjectPermissionSecretActions.DescribeAndReadValue)) { + const hasReadValue = permission.action.includes(ProjectPermissionSecretActions.ReadValue); + const hasDescribeSecret = permission.action.includes(ProjectPermissionSecretActions.DescribeSecret); + + if (!hasReadValue && !hasDescribeSecret) return; + + const hasBothDescribeAndReadValue = + permission.action.includes(ProjectPermissionSecretActions.DescribeSecret) && + permission.action.includes(ProjectPermissionSecretActions.ReadValue); + + throw new BadRequestError({ + message: `You have selected Full Read Access, and ${ + hasBothDescribeAndReadValue + ? "both Read Value and Describe Secret" + : hasReadValue + ? "Read Value" + : hasDescribeSecret + ? "Describe Secret" + : "" + }. You cannot select Read Value or Describe Secret if you have selected Full Read Access.` + }); + } + } + } +} + function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) { if (!actorAuthMethod) return false; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 3f69c481a..6256c9965 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -18,7 +18,8 @@ export enum ProjectPermissionActions { } export enum ProjectPermissionSecretActions { - DescribeSecret = "read", + DescribeAndReadValue = "read", + DescribeSecret = "describeSecret", ReadValue = "readValue", Create = "create", Edit = "edit", @@ -564,6 +565,7 @@ const buildAdminPermissionRules = () => { can( [ + // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.Create, @@ -632,6 +634,7 @@ const buildMemberPermissionRules = () => { can( [ + // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.Edit, @@ -808,6 +811,7 @@ export const projectMemberPermissions = buildMemberPermissionRules(); const buildViewerPermissionRules = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); + // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); @@ -852,7 +856,6 @@ export const buildServiceTokenProjectPermission = ( ) => { const canWrite = permission.includes("write"); const canRead = permission.includes("read"); - const canReadValue = permission.includes("readValue"); const { can, build } = new AbilityBuilder>(createMongoAbility); scopes.forEach(({ secretPath, environment }) => { @@ -876,7 +879,7 @@ export const buildServiceTokenProjectPermission = ( environment }); } - if (canRead) { + if (canRead && subject !== ProjectPermissionSub.Secrets) { can(ProjectPermissionActions.Read, subject, { // @ts-expect-error type secretPath: { $glob: secretPath }, @@ -884,12 +887,18 @@ export const buildServiceTokenProjectPermission = ( }); } - if (subject === ProjectPermissionSub.Secrets && canReadValue) { + if (subject === ProjectPermissionSub.Secrets && canRead) { // @ts-expect-error type can(ProjectPermissionSecretActions.ReadValue, subject as ProjectPermissionSub.Secrets, { secretPath: { $glob: secretPath }, environment }); + + // @ts-expect-error type + can(ProjectPermissionSecretActions.DescribeSecret, subject as ProjectPermissionSub.Secrets, { + secretPath: { $glob: secretPath }, + environment + }); } } ); diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 8efae75e6..9f3ec7111 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -77,6 +77,7 @@ import { TSecretApprovalDetailsDTO, TStatusChangeDTO } from "./secret-approval-request-types"; +import { CheckForbiddenErrorSecretsSubject } from "../permission/permission-fns"; type TSecretApprovalRequestServiceFactoryDep = { permissionService: Pick; @@ -917,10 +918,11 @@ export const secretApprovalRequestServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); + + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath + }); await projectDAL.checkProjectUpgradeStatus(projectId); diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index 397824904..6afc6bca3 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */ // akhilmhdh: I did this, quite strange bug with eslint. Everything do have a type stil has this error -import { ForbiddenError, subject } from "@casl/ability"; +import { ForbiddenError } from "@casl/ability"; import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; @@ -12,6 +12,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TSecretDALFactory } from "@app/services/secret/secret-dal"; +import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns"; import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; @@ -22,6 +23,7 @@ import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secre import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { TLicenseServiceFactory } from "../license/license-service"; +import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionActions, @@ -39,7 +41,6 @@ import { TSnapshotFolderDALFactory } from "./snapshot-folder-dal"; import { TSnapshotSecretDALFactory } from "./snapshot-secret-dal"; import { TSnapshotSecretV2DALFactory } from "./snapshot-secret-v2-dal"; import { getFullFolderPath } from "./snapshot-service-fns"; -import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns"; type TSecretSnapshotServiceFactoryDep = { snapshotDAL: TSnapshotDALFactory; @@ -102,10 +103,10 @@ export const secretSnapshotServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath: path + }); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) { @@ -139,10 +140,10 @@ export const secretSnapshotServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath: path + }); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) @@ -186,15 +187,12 @@ export const secretSnapshotServiceFactory = ({ snapshotDetails = { ...encryptedSnapshotDetails, secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => { - const canReadValue = permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: encryptedSnapshotDetails.environment.slug, - secretPath: fullFolderPath, - secretName: el.key, - secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined - }) - ); + const canReadValue = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: encryptedSnapshotDetails.environment.slug, + secretPath: fullFolderPath, + secretName: el.key, + secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined + }); let secretValue = ""; if (canReadValue) { @@ -238,15 +236,12 @@ export const secretSnapshotServiceFactory = ({ key: botKey }); - const canReadValue = permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: encryptedSnapshotDetails.environment.slug, - secretPath: fullFolderPath, - secretName: secretKey, - secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined - }) - ); + const canReadValue = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: encryptedSnapshotDetails.environment.slug, + secretPath: fullFolderPath, + secretName: secretKey, + secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined + }); let secretValue = ""; diff --git a/backend/src/server/routes/v2/service-token-router.ts b/backend/src/server/routes/v2/service-token-router.ts index 1a84c9123..fb10f17db 100644 --- a/backend/src/server/routes/v2/service-token-router.ts +++ b/backend/src/server/routes/v2/service-token-router.ts @@ -94,7 +94,7 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) => iv: z.string().trim(), tag: z.string().trim(), expiresIn: z.number().nullable(), - permissions: z.enum(["read", "write", "readValue"]).array() + permissions: z.enum(["read", "write"]).array() }), response: { 200: z.object({ diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index f72357964..3f7c55896 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -1,6 +1,7 @@ -import { ForbiddenError, subject } from "@casl/ability"; +import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; +import { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -95,13 +96,10 @@ export const integrationServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: sourceEnvironment, - secretPath - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: sourceEnvironment, + secretPath + }); const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath); if (!folder) { @@ -178,13 +176,10 @@ export const integrationServiceFactory = ({ const newSecretPath = secretPath || integration.secretPath; if (environment || secretPath) { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: newEnvironment, - secretPath: newSecretPath - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: newEnvironment, + secretPath: newSecretPath + }); } const folder = await folderDAL.findBySecretPath(integration.projectId, newEnvironment, newSecretPath); diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 5367a70cd..b21cc99fe 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -10,6 +10,7 @@ import { } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -764,10 +765,7 @@ export const projectServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSub.Secrets - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret); const project = await projectDAL.findProjectById(projectId); diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 6c412edc0..27ed12c84 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -4,6 +4,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -93,13 +94,11 @@ export const secretImportServiceFactory = ({ ); // check if user has permission to import from target path - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment: data.environment, - secretPath: data.path - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment: data.environment, + secretPath: data.path + }); + if (isReplication) { const plan = await licenseService.getPlan(actorOrgId); if (!plan.secretApproval) { @@ -405,13 +404,10 @@ export const secretImportServiceFactory = ({ if (!secretImportDoc.isReplication) throw new BadRequestError({ message: "Import is not in replication mode" }); // check if user has permission to import from target path - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment: secretImportDoc.importEnv.slug, - secretPath: secretImportDoc.importPath - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment: secretImportDoc.importEnv.slug, + secretPath: secretImportDoc.importPath + }); await projectDAL.checkProjectUpgradeStatus(projectId); @@ -599,14 +595,12 @@ export const secretImportServiceFactory = ({ // so anything based on this order will also be in right position const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false }); const allowedImports = secretImports.filter((el) => - permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: el.importEnv.slug, - secretPath: el.importPath - }) - ) + CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: el.importEnv.slug, + secretPath: el.importPath + }) ); + return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL }); }; @@ -651,16 +645,14 @@ export const secretImportServiceFactory = ({ secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ) + CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) }); + return importedSecrets; } @@ -671,13 +663,10 @@ export const secretImportServiceFactory = ({ }); const allowedImports = secretImports.filter((el) => - permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: el.importEnv.slug, - secretPath: el.importPath - }) - ) + CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: el.importEnv.slug, + secretPath: el.importPath + }) ); const importedSecrets = await fnSecretsFromImports({ allowedImports, diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index 7b92a463c..d74ea120e 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -1,6 +1,7 @@ -import { ForbiddenError, subject } from "@casl/ability"; +import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; +import { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionSecretActions, @@ -178,13 +179,10 @@ export const secretSyncServiceFactory = ({ ProjectPermissionSub.SecretSyncs ); - ForbiddenError.from(projectPermission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath - }) - ); + CheckForbiddenErrorSecretsSubject(projectPermission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath + }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -269,13 +267,10 @@ export const secretSyncServiceFactory = ({ if (!updatedEnvironment || !updatedSecretPath) throw new BadRequestError({ message: "Must specify both source environment and secret path" }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: updatedEnvironment, - secretPath: updatedSecretPath - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: updatedEnvironment, + secretPath: updatedSecretPath + }); const newFolder = await folderDAL.findBySecretPath(secretSync.projectId, updatedEnvironment, updatedSecretPath); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 3b7f04907..8ebc0a582 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -10,6 +10,7 @@ import { TableName, TSecretsV2 } from "@app/db/schemas"; +import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -540,17 +541,14 @@ export const secretV2BridgeServiceFactory = ({ }); } - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: inputSecret.secretName, - ...(tagsToCheck.length && { - secretTags: tagsToCheck.map((el) => el.slug) - }) + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath, + secretName: inputSecret.secretName, + ...(tagsToCheck.length && { + secretTags: tagsToCheck.map((el) => el.slug) }) - ); + }); return reshapeBridgeSecret( projectId, @@ -651,15 +649,12 @@ export const secretV2BridgeServiceFactory = ({ projectId }); - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: secretToDelete.key, - secretTags: secretToDelete.tags?.map((el) => el.slug) - }) - ); + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath, + secretName: secretToDelete.key, + secretTags: secretToDelete.tags?.map((el) => el.slug) + }); return reshapeBridgeSecret( projectId, @@ -702,11 +697,7 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSub.Secrets - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -752,11 +743,7 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSub.Secrets - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) return 0; @@ -791,8 +778,20 @@ export const secretV2BridgeServiceFactory = ({ }); const decryptedSecrets = secrets - .filter((el) => - projectPermission.can( + .filter((el) => { + if ( + filterByAction === ProjectPermissionSecretActions.ReadValue || + filterByAction === ProjectPermissionSecretActions.DescribeSecret + ) { + return CheckCanSecretsSubject(projectPermission, filterByAction, { + environment: groupedFolderMappings[el.folderId][0].environment, + secretPath: groupedFolderMappings[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }); + } + + return projectPermission.can( filterByAction, subject(ProjectPermissionSub.Secrets, { environment: groupedFolderMappings[el.folderId][0].environment, @@ -800,19 +799,16 @@ export const secretV2BridgeServiceFactory = ({ secretName: el.key, secretTags: el.tags.map((i) => i.slug) }) - ) - ) + ); + }) .map((secret) => { // Note(Daniel): This is only relevant if the filterAction isn't set to ReadValue. This is needed for the frontend. - const secretValueHidden = !projectPermission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: groupedFolderMappings[secret.folderId][0].environment, - secretPath: groupedFolderMappings[secret.folderId][0].path, - secretName: secret.key, - secretTags: secret.tags.map((i) => i.slug) - }) - ); + const secretValueHidden = !CheckCanSecretsSubject(projectPermission, ProjectPermissionSecretActions.ReadValue, { + environment: groupedFolderMappings[secret.folderId][0].environment, + secretPath: groupedFolderMappings[secret.folderId][0].path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }); return reshapeBridgeSecret( projectId, @@ -858,10 +854,7 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); if (!isInternal) { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSub.Secrets - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -913,15 +906,11 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: path, - secretTags: params.tagSlugs - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath: path, + secretTags: params.tagSlugs + }); let paths: { folderId: string; path: string }[] = []; @@ -960,15 +949,12 @@ export const secretV2BridgeServiceFactory = ({ const decryptedSecrets = secrets .filter((el) => { - const canDescribeSecret = permission.can( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: groupedPaths[el.folderId][0].path, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }) - ); + const canDescribeSecret = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }); if (!canDescribeSecret) { return false; @@ -977,14 +963,15 @@ export const secretV2BridgeServiceFactory = ({ if (viewSecretValue) { // Recursive secret, should be filtered out if (groupedPaths[el.folderId][0].path !== path) { - const canReadRecursiveSecretValue = permission.can( + const canReadRecursiveSecretValue = CheckCanSecretsSubject( + permission, ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { + { environment, secretPath: groupedPaths[el.folderId][0].path, secretName: el.key, secretTags: el.tags.map((i) => i.slug) - }) + } ); if (!canReadRecursiveSecretValue) { @@ -993,15 +980,12 @@ export const secretV2BridgeServiceFactory = ({ } if (throwOnMissingReadValuePermission) { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: groupedPaths[el.folderId][0].path, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }); } // Else, we do nothing. Because we don't want to filter out the secret, OR throw an error. // If the user doesn't have access to read the value, in the below map function, we mask the secret value and return the secret with a hidden value. @@ -1014,15 +998,12 @@ export const secretV2BridgeServiceFactory = ({ const secretValueHidden = !viewSecretValue || - !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: groupedPaths[secret.folderId][0].path, - secretName: secret.key, - secretTags: secret.tags.map((i) => i.slug) - }) - ); + !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: groupedPaths[secret.folderId][0].path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }); return reshapeBridgeSecret( projectId, @@ -1047,15 +1028,12 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ) + CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) }); if (shouldExpandSecretReferences) { @@ -1095,25 +1073,19 @@ export const secretV2BridgeServiceFactory = ({ expandSecretReferences, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - const canDescribe = permission.can( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ); + const canDescribe = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }); - const canReadValue = permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ); + const canReadValue = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }); return viewSecretValue ? canDescribe && canReadValue : canDescribe; } @@ -1264,15 +1236,12 @@ export const secretV2BridgeServiceFactory = ({ }) )); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: path, - secretName, - secretTags: (secret?.tags || []).map((el) => el.slug) - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath: path, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }); // this will throw if the user doesn't have read value permission no matter what // because if its an expansion, it will fully depend on the value. @@ -1282,15 +1251,12 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - return permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ); + return CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }); } }); @@ -1310,15 +1276,12 @@ export const secretV2BridgeServiceFactory = ({ decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined, hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - return permission.can( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ); + return CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }); } }); @@ -1330,15 +1293,12 @@ export const secretV2BridgeServiceFactory = ({ if (viewSecretValue) { if ( - !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: importedSecret.environment, - secretPath: importedSecrets[i].secretPath, - secretName: importedSecret.key, - secretTags: (importedSecret.secretTags || []).map((el) => el.slug) - }) - ) && + !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: importedSecret.environment, + secretPath: importedSecrets[i].secretPath, + secretName: importedSecret.key, + secretTags: (importedSecret.secretTags || []).map((el) => el.slug) + }) && secretType !== SecretType.Personal ) { throw new ForbiddenRequestError({ @@ -1386,15 +1346,12 @@ export const secretV2BridgeServiceFactory = ({ if (viewSecretValue) { if ( - !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: path, - secretName, - secretTags: (secret?.tags || []).map((el) => el.slug) - }) - ) && + !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: path, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }) && secretType !== SecretType.Personal ) { throw new ForbiddenRequestError({ @@ -1562,15 +1519,12 @@ export const secretV2BridgeServiceFactory = ({ }); return newSecrets.map((el) => { - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: el.key, - secretTags: el.tags?.map((i) => i.slug) - }) - ); + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags?.map((i) => i.slug) + }); return reshapeBridgeSecret( projectId, @@ -1899,15 +1853,12 @@ export const secretV2BridgeServiceFactory = ({ ); return updatedSecrets.map((el) => { - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: el.secretPath, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }) - ); + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: el.secretPath, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }); return { ...reshapeBridgeSecret( @@ -2032,15 +1983,12 @@ export const secretV2BridgeServiceFactory = ({ const secretValueHidden = !secretToDeleteMatch || - !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: el.key, - secretTags: secretToDeleteMatch.tags?.map((i) => i.slug) - }) - ); + !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath, + secretName: el.key, + secretTags: secretToDeleteMatch.tags?.map((i) => i.slug) + }); return reshapeBridgeSecret( projectId, @@ -2097,17 +2045,15 @@ export const secretV2BridgeServiceFactory = ({ sort: [["createdAt", "desc"]] }); return secretVersions.map((el) => { - const secretValueHidden = permission.cannot( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: folder.environment.envSlug, - secretPath: folderWithPath.path, - secretName: el.key, - ...(el.tags?.length && { - secretTags: el.tags.map((tag) => tag.slug) - }) + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: folder.environment.envSlug, + secretPath: folderWithPath.path, + secretName: el.key, + ...(el.tags?.length && { + secretTags: el.tags.map((tag) => tag.slug) }) - ); + }); + return reshapeBridgeSecret( folder.projectId, folder.environment.envSlug, @@ -2224,15 +2170,27 @@ export const secretV2BridgeServiceFactory = ({ sourceSecrets.forEach((secret) => { for (const sourceAction of sourceActions) { - ForbiddenError.from(permission).throwUnlessCan( - sourceAction, - subject(ProjectPermissionSub.Secrets, { + if ( + sourceAction === ProjectPermissionSecretActions.DescribeSecret || + sourceAction === ProjectPermissionSecretActions.ReadValue + ) { + CheckForbiddenErrorSecretsSubject(permission, sourceAction, { environment: sourceEnvironment, secretPath: sourceSecretPath, secretName: secret.key, secretTags: secret.tags.map((el) => el.slug) - }) - ); + }); + } else { + ForbiddenError.from(permission).throwUnlessCan( + sourceAction, + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + } } }); @@ -2555,10 +2513,10 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath + }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -2579,15 +2537,12 @@ export const secretV2BridgeServiceFactory = ({ type: SecretType.Shared }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName, - secretTags: (secret?.tags || []).map((el) => el.slug) - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }); const decryptedSecretValue = secret.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() @@ -2599,27 +2554,21 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretName, expandSecretTags) => - permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretName, - secretTags: expandSecretTags - }) - ) + CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretName, + secretTags: expandSecretTags + }) }); if ( - !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName, - secretTags: (secret?.tags || []).map((el) => el.slug) - }) - ) + !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }) ) { throw new ForbiddenRequestError({ message: `Unable to get secret reference tree for secret with key '${secretName}', because you don't have permission to view secret value.` diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 4556528a8..ddaa054ab 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -1,5 +1,4 @@ /* eslint-disable no-await-in-loop */ -import { subject } from "@casl/ability"; import path from "path"; import { @@ -12,8 +11,9 @@ import { TSecretFolders, TSecrets } from "@app/db/schemas"; +import { CheckCanSecretsSubject } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { buildSecretBlindIndexFromName, @@ -191,13 +191,10 @@ export const recursivelyGetSecretPaths = ({ // Filter out paths that the user does not have permission to access, and paths that are not in the current path const allowedPaths = paths.filter( (folder) => - permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: folder.path - }) - ) && folder.path.startsWith(currentPath === "/" ? "" : currentPath) + CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: folder.path + }) && folder.path.startsWith(currentPath === "/" ? "" : currentPath) ); return allowedPaths; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 8af0deb2b..e3eb75bda 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -13,6 +13,7 @@ import { SecretType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -452,13 +453,10 @@ export const secretServiceFactory = ({ }); } - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: path - }) - ); + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: path + }); return { ...updatedSecret[0], @@ -562,10 +560,10 @@ export const secretServiceFactory = ({ }); } - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: path + }); return { ...deletedSecret[0], @@ -622,10 +620,10 @@ export const secretServiceFactory = ({ paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p })); } else { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: path + }); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) return { secrets: [], imports: [] }; @@ -647,13 +645,10 @@ export const secretServiceFactory = ({ // if its service token allow full access over imported one actor === ActorType.SERVICE ? true - : permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: importEnv.slug, - secretPath: importPath - }) - ) + : CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: importEnv.slug, + secretPath: importPath + }) ); const importedSecrets = await fnSecretsFromImports({ allowedImports, @@ -704,10 +699,11 @@ export const secretServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: path + }); + const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new NotFoundError({ @@ -754,14 +750,12 @@ export const secretServiceFactory = ({ // if its service token allow full access over imported one actor === ActorType.SERVICE ? true - : permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: importEnv.slug, - secretPath: importPath - }) - ) + : CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: importEnv.slug, + secretPath: importPath + }) ); + const importedSecrets = await fnSecretsFromImports({ allowedImports, secretDAL, @@ -975,10 +969,10 @@ export const secretServiceFactory = ({ secretVersionTagDAL }); - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: path + }); return updatedSecrets.map((secret) => ({ ...secret, @@ -1069,11 +1063,10 @@ export const secretServiceFactory = ({ }); } } - - const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: path + }); return secrets.map((secret) => ({ ...secret, @@ -1259,8 +1252,20 @@ export const secretServiceFactory = ({ ProjectPermissionSecretActions.Delete, ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit - ].filter((action) => - entityPermission.permission.can( + ].filter((action) => { + if ( + action === ProjectPermissionSecretActions.DescribeSecret || + action === ProjectPermissionSecretActions.ReadValue + ) { + return CheckCanSecretsSubject(entityPermission.permission, action, { + environment, + secretPath, + secretName, + secretTags: secret?.tags?.map((el) => el.slug) + }); + } + + return entityPermission.permission.can( action, subject(ProjectPermissionSub.Secrets, { environment, @@ -1268,8 +1273,8 @@ export const secretServiceFactory = ({ secretName, secretTags: secret?.tags?.map((el) => el.slug) }) - ) - ); + ); + }); return { ...entityPermission, @@ -2424,17 +2429,14 @@ export const secretServiceFactory = ({ key: botKey }); - const secretValueHidden = permission.cannot( - ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { - environment: folder.environment.envSlug, - secretPath: folderWithPath.path, - secretName: secretKey, - ...(el.tags?.length && { - secretTags: el.tags.map((tag) => tag.slug) - }) + const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: folder.environment.envSlug, + secretPath: folderWithPath.path, + secretName: secretKey, + ...(el.tags?.length && { + secretTags: el.tags.map((tag) => tag.slug) }) - ); + }); return decryptSecretRaw( { @@ -2833,13 +2835,23 @@ export const secretServiceFactory = ({ } for (const sourceAction of sourceActions) { - ForbiddenError.from(permission).throwUnlessCan( - sourceAction, - subject(ProjectPermissionSub.Secrets, { + if ( + sourceAction === ProjectPermissionSecretActions.ReadValue || + sourceAction === ProjectPermissionSecretActions.DescribeSecret + ) { + CheckForbiddenErrorSecretsSubject(permission, sourceAction, { environment: sourceEnvironment, secretPath: sourceSecretPath - }) - ); + }); + } else { + ForbiddenError.from(permission).throwUnlessCan( + sourceAction, + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath + }) + ); + } } return { diff --git a/backend/src/services/service-token/service-token-types.ts b/backend/src/services/service-token/service-token-types.ts index d06d90a9d..f1a700908 100644 --- a/backend/src/services/service-token/service-token-types.ts +++ b/backend/src/services/service-token/service-token-types.ts @@ -7,7 +7,7 @@ export type TCreateServiceTokenDTO = { iv: string; tag: string; expiresIn?: number | null; - permissions: ("read" | "write" | "readValue")[]; + permissions: ("read" | "write")[]; } & TProjectPermission; export type TGetServiceTokenInfoDTO = Omit; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 260ffce09..495055977 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -8,7 +8,8 @@ export enum ProjectPermissionActions { } export enum ProjectPermissionSecretActions { - DescribeSecret = "read", + DescribeAndReadValue = "read", + DescribeSecret = "describeSecret", ReadValue = "readValue", Create = "create", Edit = "edit", diff --git a/frontend/src/lib/fn/permission.ts b/frontend/src/lib/fn/permission.ts new file mode 100644 index 000000000..f2077d76b --- /dev/null +++ b/frontend/src/lib/fn/permission.ts @@ -0,0 +1,36 @@ +import { MongoAbility, subject } from "@casl/ability"; + +import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; +import { + ProjectPermissionSecretActions, + ProjectPermissionSub, + SecretSubjectFields +} from "@app/context/ProjectPermissionContext/types"; + +export function secretsPermissionCan( + permission: MongoAbility, + action: Extract< + ProjectPermissionSecretActions, + ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue + >, + subjectFields?: SecretSubjectFields +) { + let canNewPermission = false; + let canOldPermission = false; + + if (subjectFields) { + canNewPermission = permission.can(action, subject(ProjectPermissionSub.Secrets, subjectFields)); + canOldPermission = permission.can( + ProjectPermissionSecretActions.DescribeAndReadValue, + subject(ProjectPermissionSub.Secrets, subjectFields) + ); + } else { + canNewPermission = permission.can(action, ProjectPermissionSub.Secrets); + canOldPermission = permission.can( + ProjectPermissionSecretActions.DescribeAndReadValue, + ProjectPermissionSub.Secrets + ); + } + + return canNewPermission || canOldPermission; +} diff --git a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx index a669a8360..fa9500a53 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/AddServiceTokenModal.tsx @@ -303,13 +303,9 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => { render={({ field: { onChange, value }, fieldState: { error } }) => { const options = [ { - label: "Describe Secret (default)", + label: "Read (default)", value: "read" }, - { - label: "Read Value (optional)", - value: "readValue" - }, { label: "Write (optional)", value: "write" diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx index 8aca40a34..a6e472976 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx @@ -35,12 +35,13 @@ export const GeneralPermissionPolicies = ) => { - const { control } = useFormContext(); + const { control, watch } = useFormContext(); const items = useFieldArray({ control, name: `permissions.${subject}` }); const [isOpen, setIsOpen] = useToggle(); + // const [hideFullReadAccess, setHideFullReadAccess] = useState(false); if (!items.fields.length) return
; @@ -71,119 +72,138 @@ export const GeneralPermissionPolicies = {isOpen && (
- {items.fields.map((el, rootIndex) => ( -
- {isConditionalSubjects(subject) && ( -
-
Permission
-
- ( - - )} - /> -
-
- -

- Whether to allow or forbid the selected actions when the following - conditions (if any) are met. -

-

Forbid rules must come after allow rules.

- - } - > - -
-
-
- )} -
-
Actions
-
- {actions.map(({ label, value }) => { - if (typeof value !== "string") return undefined; + {items.fields.map((el, rootIndex) => { + let isFullReadAccessEnabled = false; - return ( + if (subject === ProjectPermissionSub.Secrets) { + isFullReadAccessEnabled = watch(`permissions.${subject}.${rootIndex}.read` as any); + } + + return ( +
+ {isConditionalSubjects(subject) && ( +
+
Permission
+
{ - return ( -
- - {label} - -
- ); - }} + defaultValue={false as any} + name={`permissions.${subject}.${rootIndex}.inverted`} + render={({ field }) => ( + + )} /> - ); +
+
+ +

+ Whether to allow or forbid the selected actions when the following + conditions (if any) are met. +

+

Forbid rules must come after allow rules.

+ + } + > + +
+
+
+ )} +
+
Actions
+
+ {actions.map(({ label, value }, index) => { + if (typeof value !== "string") return undefined; + + if ( + subject === ProjectPermissionSub.Secrets && + value === "read" && + !isFullReadAccessEnabled + ) { + return null; + } + + return ( + { + return ( +
+ + {label} + +
+ ); + }} + /> + ); + })} +
+
+ {children && + cloneElement(children, { + position: rootIndex })} +
+ {!isDisabled && isConditionalSubjects(subject) && ( + + )} + {!isDisabled && ( + + )}{" "}
- {children && - cloneElement(children, { - position: rootIndex - })} -
- {!isDisabled && isConditionalSubjects(subject) && ( - - )} - {!isDisabled && ( - - )}{" "} -
-
- ))} + ); + })}
)}
diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index e26ec8bc4..9f13b53a8 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -1,5 +1,9 @@ +import { ReactNode } from "react"; +import { faWarning } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { z } from "zod"; +import { Tooltip } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionCmekActions, @@ -24,11 +28,12 @@ const GeneralPolicyActionSchema = z.object({ }); const SecretPolicyActionSchema = z.object({ - read: z.boolean().optional(), // describe secret - edit: z.boolean().optional(), - delete: z.boolean().optional(), - create: z.boolean().optional(), - readValue: z.boolean().optional() + [ProjectPermissionSecretActions.DescribeAndReadValue]: z.boolean().optional(), // existing read, gives both describe and read value + [ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(), // describe secret, cannot read value + [ProjectPermissionSecretActions.ReadValue]: z.boolean().optional(), // read value + [ProjectPermissionSecretActions.Edit]: z.boolean().optional(), // edit secret + [ProjectPermissionSecretActions.Delete]: z.boolean().optional(), // delete secret + [ProjectPermissionSecretActions.Create]: z.boolean().optional() // create secret }); const CmekPolicyActionSchema = z.object({ @@ -294,19 +299,24 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { } if (subject === ProjectPermissionSub.Secrets) { - const canRead = action.includes(ProjectPermissionSecretActions.DescribeSecret); + const canDescribeAndReadValue = action.includes( + ProjectPermissionSecretActions.DescribeAndReadValue + ); + const canDescribe = action.includes(ProjectPermissionSecretActions.DescribeSecret); + const canReadValue = action.includes(ProjectPermissionSecretActions.ReadValue); + const canEdit = action.includes(ProjectPermissionSecretActions.Edit); const canDelete = action.includes(ProjectPermissionSecretActions.Delete); const canCreate = action.includes(ProjectPermissionSecretActions.Create); - const canReadValue = action.includes(ProjectPermissionSecretActions.ReadValue); // from above statement we are sure it won't be undefined formVal[subject]!.push({ - read: canRead, + describeSecret: canDescribe, + read: canDescribeAndReadValue, + readValue: canReadValue, create: canCreate, edit: canEdit, delete: canDelete, - readValue: canReadValue, conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], inverted }); @@ -501,7 +511,7 @@ export type TProjectPermissionObject = { [K in ProjectPermissionSub]: { title: string; actions: { - label: string; + label: string | ReactNode; value: keyof Omit< NonNullable[K]>[number], "conditions" | "inverted" @@ -514,11 +524,35 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { [ProjectPermissionSub.Secrets]: { title: "Secrets", actions: [ - { label: "Describe Secret", value: "read" }, - { label: "Create", value: "create" }, - { label: "Read Value", value: "readValue" }, - { label: "Modify", value: "edit" }, - { label: "Remove", value: "delete" } + { + label: ( +
+

+ Read (legacy) +

+ + This is a legacy action and will be removed in the future. +
+
You should instead use the{" "} + Describe Secret and{" "} + Read Value actions. +
+ } + > + + +
+ ), + value: ProjectPermissionSecretActions.DescribeAndReadValue + }, + { label: "Describe Secret", value: ProjectPermissionSecretActions.DescribeSecret }, + { label: "Read Value", value: ProjectPermissionSecretActions.ReadValue }, + { label: "Modify", value: ProjectPermissionSecretActions.Edit }, + { label: "Remove", value: ProjectPermissionSecretActions.Delete }, + { label: "Create", value: ProjectPermissionSecretActions.Create } ] }, [ProjectPermissionSub.SecretFolders]: { diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx index 3b05f10fa..536f75542 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx @@ -15,6 +15,7 @@ import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionCo import { useToggle } from "@app/hooks"; import { useUpdateSecretV3 } from "@app/hooks/api"; import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types"; +import { secretsPermissionCan } from "@app/lib/fn/permission"; enum SecretActionType { Created = "created", @@ -51,8 +52,11 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath } secretTags: (secretDetails?.tags || []).map((i) => i.slug) }); const isSecretInEnvReadOnly = - permission.can(ProjectPermissionSecretActions.DescribeSecret, secretPermissionSubject) && - permission.cannot(ProjectPermissionSecretActions.Edit, secretPermissionSubject); + secretsPermissionCan( + permission, + ProjectPermissionSecretActions.DescribeSecret, + secretPermissionSubject + ) && permission.cannot(ProjectPermissionSecretActions.Edit, secretPermissionSubject); if (isSecretInEnvReadOnly) { return true; } diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index 5cbefe9c3..4dba1c2a8 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -38,6 +38,7 @@ import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard"; import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { ProjectType } from "@app/hooks/api/workspace/types"; +import { secretsPermissionCan } from "@app/lib/fn/permission"; import { SecretTableResourceCount } from "../OverviewPage/components/SecretTableResourceCount"; import { SecretV2MigrationSection } from "../OverviewPage/components/SecretV2MigrationSection"; @@ -103,23 +104,27 @@ const Page = () => { const workspaceId = currentWorkspace?.id || ""; const projectSlug = currentWorkspace?.slug || ""; const secretPath = (routerQueryParams.secretPath as string) || "/"; - const canReadSecret = permission.can( + + const canReadSecret = secretsPermissionCan( + permission, ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { + { environment, secretPath, secretName: "*", secretTags: ["*"] - }) + } ); - const canReadSecretValue = permission.can( + + const canReadSecretValue = secretsPermissionCan( + permission, ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { + { environment, secretPath, secretName: "*", secretTags: ["*"] - }) + } ); const canReadSecretImports = permission.can( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index d6da791bb..3ad7159d7 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -57,6 +57,7 @@ import { ActorType } from "@app/hooks/api/auditLogs/enums"; import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries"; import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types"; import { ProjectType } from "@app/hooks/api/workspace/types"; +import { secretsPermissionCan } from "@app/lib/fn/permission"; import { CreateReminderForm } from "./CreateReminderForm"; import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; @@ -140,26 +141,24 @@ export const SecretDetailSidebar = ({ }) ); - const cannotReadSecretValue = permission.cannot( + const cannotReadSecretValue = !secretsPermissionCan( + permission, ProjectPermissionSecretActions.ReadValue, - subject(ProjectPermissionSub.Secrets, { + { environment, secretPath, secretName: secretKey, secretTags: selectTagSlugs - }) + } ); const isReadOnly = - permission.can( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: secretKey, - secretTags: selectTagSlugs - }) - ) && + secretsPermissionCan(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath, + secretName: secretKey, + secretTags: selectTagSlugs + }) && cannotEditSecret && cannotReadSecretValue; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx index 02f7a0aa9..0271bffef 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx @@ -47,6 +47,7 @@ import { import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { Blur } from "@app/components/v2/Blur"; +import { secretsPermissionCan } from "@app/lib/fn/permission"; import { FontAwesomeSpriteName, formSchema, @@ -131,15 +132,12 @@ export const SecretItem = memo( }); const isReadOnly = - permission.can( - ProjectPermissionSecretActions.DescribeSecret, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName, - secretTags: selectedTagSlugs - }) - ) && + secretsPermissionCan(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath, + secretName, + secretTags: selectedTagSlugs + }) && permission.cannot( ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { From 955657e17234278a21c7cd5c6434d0cc1a5ba1bc Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 8 Mar 2025 03:58:15 +0400 Subject: [PATCH 04/15] fix: legacy permission check --- .../secret-v2-bridge/secret-v2-bridge-service.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 8ebc0a582..d6115cdcc 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1127,15 +1127,12 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: folderWithPath.environmentSlug, - secretPath: folderWithPath.path, - secretName: secret.key, - secretTags: secret.tags.map((i) => i.slug) - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: folderWithPath.environmentSlug, + secretPath: folderWithPath.path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }); if (secret.type === SecretType.Personal && secret.userId !== actorId) { throw new ForbiddenRequestError({ From dc88115d4370ad5cd75c6e90031116d75991541a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 8 Mar 2025 03:59:34 +0400 Subject: [PATCH 05/15] fix: tests failing --- backend/e2e-test/routes/v2/service-token.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/e2e-test/routes/v2/service-token.spec.ts b/backend/e2e-test/routes/v2/service-token.spec.ts index bfe043523..6cc8f6e34 100644 --- a/backend/e2e-test/routes/v2/service-token.spec.ts +++ b/backend/e2e-test/routes/v2/service-token.spec.ts @@ -6,7 +6,7 @@ import { decryptAsymmetric, decryptSymmetric128BitHexKeyUTF8, encryptSymmetric12 const createServiceToken = async ( scopes: { environment: string; secretPath: string }[], - permissions: ("read" | "write" | "readValue")[] + permissions: ("read" | "write")[] ) => { const projectKeyRes = await testServer.inject({ method: "GET", @@ -139,7 +139,7 @@ describe("Service token secret ops", async () => { beforeAll(async () => { serviceToken = await createServiceToken( [{ secretPath: "/**", environment: seedData1.environment.slug }], - ["read", "write", "readValue"] + ["read", "write"] ); // this is ensure cli service token decryptiong working fine @@ -496,7 +496,7 @@ describe("Service token fail cases", async () => { test("Unauthorized secret path access", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read", "readValue", "write"] + ["read", "write"] ); const fetchSecrets = await testServer.inject({ method: "GET", @@ -518,7 +518,7 @@ describe("Service token fail cases", async () => { test("Unauthorized secret environment access", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read", "readValue", "write"] + ["read", "write"] ); const fetchSecrets = await testServer.inject({ method: "GET", @@ -540,7 +540,7 @@ describe("Service token fail cases", async () => { test("Unauthorized write operation", async () => { const serviceToken = await createServiceToken( [{ secretPath: "/", environment: seedData1.environment.slug }], - ["read", "readValue"] + ["read"] ); const writeSecrets = await testServer.inject({ method: "POST", From 46ce46b5a04e97d7c8d953698702d328af0df088 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 8 Mar 2025 04:04:52 +0400 Subject: [PATCH 06/15] fix: get secret by ID using legacy permissions --- backend/src/server/routes/v3/secret-router.ts | 9 +-------- .../secret-v2-bridge/secret-v2-bridge-service.ts | 16 +++++++++++----- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index b68d042f0..f854baade 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -368,14 +368,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { 200: z.object({ secret: secretRawSchema.extend({ secretPath: z.string(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional(), + tags: SanitizedTagSchema.array().optional(), secretMetadata: ResourceMetadataSchema.optional() }) }) diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index d6115cdcc..ec2728966 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1154,11 +1154,17 @@ export const secretV2BridgeServiceFactory = ({ ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() : ""; - return reshapeBridgeSecret(secret.projectId, folderWithPath.environmentSlug, folderWithPath.path, { - ...secret, - value: secretValue, - comment: secretComment - }); + return reshapeBridgeSecret( + secret.projectId, + folderWithPath.environmentSlug, + folderWithPath.path, + { + ...secret, + value: secretValue, + comment: secretComment + }, + false + ); }; const getSecretByName = async ({ From a8fc0e540a92997b0948cead7ad07c4b6217cd0e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 10 Mar 2025 17:50:43 +0400 Subject: [PATCH 07/15] fix: tests and missing tags permission check --- backend/package.json | 1 + .../ee/services/permission/permission-fns.ts | 4 ++-- .../secret-v2-bridge-service.ts | 23 ++++++++++--------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/backend/package.json b/backend/package.json index 5c2c805bc..25380f896 100644 --- a/backend/package.json +++ b/backend/package.json @@ -71,6 +71,7 @@ "migrate:org": "tsx ./scripts/migrate-organization.ts", "seed:new": "tsx ./scripts/create-seed-file.ts", "seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run", + "seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run", "db:reset": "npm run migration:rollback -- --all && npm run migration:latest" }, "keywords": [], diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index 97113a562..c9f3b826e 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -1,5 +1,5 @@ /* eslint-disable no-nested-ternary */ -import { ForbiddenError, MongoAbility, subject } from "@casl/ability"; +import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability"; import { z } from "zod"; import { TOrganizations } from "@app/db/schemas"; @@ -15,7 +15,7 @@ import { } from "./project-permission"; export function CheckForbiddenErrorSecretsSubject( - permission: MongoAbility, + permission: MongoAbility | PureAbility, action: Extract< ProjectPermissionSecretActions, ProjectPermissionSecretActions.ReadValue | ProjectPermissionSecretActions.DescribeSecret diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index ec2728966..3b3a147bd 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1,4 +1,4 @@ -import { ForbiddenError, PureAbility, subject } from "@casl/ability"; +import { ForbiddenError, MongoAbility, subject } from "@casl/ability"; import { Knex } from "knex"; import { z } from "zod"; @@ -15,6 +15,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { ProjectPermissionActions, ProjectPermissionSecretActions, + ProjectPermissionSet, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; @@ -123,7 +124,7 @@ export const secretV2BridgeServiceFactory = ({ }: TSecretV2BridgeServiceFactoryDep) => { const $validateSecretReferences = async ( projectId: string, - permission: PureAbility, + permission: MongoAbility, references: ReturnType["nestedReferences"], tx?: Knex ) => { @@ -131,6 +132,7 @@ export const secretV2BridgeServiceFactory = ({ const uniqueReferenceEnvironmentSlugs = Array.from(new Set(references.map((el) => el.environment))); const referencesEnvironments = await projectEnvDAL.findBySlugs(projectId, uniqueReferenceEnvironmentSlugs, tx); + if (referencesEnvironments.length !== uniqueReferenceEnvironmentSlugs.length) throw new BadRequestError({ message: `Referenced environment not found. Missing ${diff( @@ -147,6 +149,7 @@ export const secretV2BridgeServiceFactory = ({ })), tx ); + const referencesFolderGroupByPath = groupBy(referredFolders.filter(Boolean), (i) => `${i?.envId}-${i?.path}`); const referredSecrets = await secretDAL.find( { @@ -194,15 +197,12 @@ export const secretV2BridgeServiceFactory = ({ const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key); references.forEach((el) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: el.environment, - secretPath: el.secretPath, - secretName: el.secretKey, - tags: referredSecretsGroupBySecretKey[el.secretKey][0]?.tags?.map((i) => i.slug) - }) - ); + CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + environment: el.environment, + secretPath: el.secretPath, + secretName: el.secretKey, + secretTags: referredSecretsGroupBySecretKey[el.secretKey][0]?.tags?.map((i) => i.slug) + }); }); return referredSecrets; @@ -277,6 +277,7 @@ export const secretV2BridgeServiceFactory = ({ const allSecretReferences = nestedReferences.concat( localReferences.map((el) => ({ secretKey: el, secretPath, environment })) ); + await $validateSecretReferences(projectId, permission, allSecretReferences); const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ From 09df4406131926d6aabb60e1c010b1f99bd6d0d6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 11 Mar 2025 00:18:42 +0400 Subject: [PATCH 08/15] Update secret-version-dal.ts --- .../secret-v2-bridge/secret-version-dal.ts | 55 +++++++++++++++---- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/backend/src/services/secret-v2-bridge/secret-version-dal.ts b/backend/src/services/secret-v2-bridge/secret-version-dal.ts index 12c54cd7c..b54b073a6 100644 --- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts @@ -187,6 +187,17 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { `${TableName.SecretVersionV2}.userActorId` ) .leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`) + .leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`) + .leftJoin( + TableName.SecretV2JnTag, + `${TableName.SecretV2}.id`, + `${TableName.SecretV2JnTag}.${TableName.SecretV2}Id` + ) + .leftJoin( + TableName.SecretTag, + `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, + `${TableName.SecretTag}.id` + ) .where((qb) => { void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId); void qb.where(`${TableName.ProjectMembership}.projectId`, projectId); @@ -197,9 +208,12 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { }) .select( selectAllTableCols(TableName.SecretVersionV2), - `${TableName.Users}.username as userActorName`, - `${TableName.Identity}.name as identityActorName`, - `${TableName.ProjectMembership}.id as membershipId` + db.ref("username").withSchema(TableName.Users).as("userActorName"), + db.ref("name").withSchema(TableName.Identity).as("identityActorName"), + db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"), + db.ref("id").withSchema(TableName.SecretTag).as("tagId"), + db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), + db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") ); if (limit) void query.limit(limit); @@ -214,14 +228,33 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { ); } - const docs: Array< - TSecretVersionsV2 & { - userActorName: string | undefined | null; - identityActorName: string | undefined | null; - membershipId: string | undefined | null; - } - > = await query; - return docs; + const docs = await query; + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ + _id: el.id, + ...SecretVersionsV2Schema.parse(el), + userActorName: el.userActorName, + identityActorName: el.identityActorName, + membershipId: el.membershipId + }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + + return data; } catch (error) { throw new DatabaseError({ error, name: "FindVersionsBySecretIdWithActors" }); } From 78c349c09a9f12b8197e315db69b9e23ecd22f20 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 11 Mar 2025 00:31:21 +0400 Subject: [PATCH 09/15] fix(view-secret-value): requested changes --- .../ee/services/permission/permission-fns.ts | 12 +- .../services/permission/project-permission.ts | 6 +- .../secret-approval-request-service.ts | 4 +- .../secret-snapshot-service.ts | 41 ++-- .../integration/integration-service.ts | 6 +- .../src/services/project/project-service.ts | 4 +- .../secret-import/secret-import-service.ts | 15 +- .../secret-sync/secret-sync-service.ts | 6 +- .../secret-v2-bridge-service.ts | 229 ++++++++++-------- .../secret-v2-bridge-types.ts | 2 +- backend/src/services/secret/secret-fns.ts | 4 +- backend/src/services/secret/secret-service.ts | 85 ++++--- frontend/src/lib/fn/permission.ts | 2 +- .../ProjectRoleModifySection.utils.tsx | 10 +- .../SecretRenameRow.tsx | 4 +- .../SecretDashboardPage.tsx | 6 +- .../SecretListView/SecretDetailSidebar.tsx | 24 +- .../components/SecretListView/SecretItem.tsx | 18 +- 18 files changed, 273 insertions(+), 205 deletions(-) diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index c9f3b826e..ecc8b676e 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -14,7 +14,7 @@ import { SecretSubjectFields } from "./project-permission"; -export function CheckForbiddenErrorSecretsSubject( +export function throwIfMissingSecretReadValueOrDescribePermission( permission: MongoAbility | PureAbility, action: Extract< ProjectPermissionSecretActions, @@ -43,7 +43,7 @@ export function CheckForbiddenErrorSecretsSubject( } } -export function CheckCanSecretsSubject( +export function hasSecretReadValueOrDescribePermission( permission: MongoAbility, action: Extract< ProjectPermissionSecretActions, @@ -83,12 +83,10 @@ export function checkForInvalidPermissionCombination(permissions: z.infer { can( [ - // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue + ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.Create, @@ -634,7 +634,7 @@ const buildMemberPermissionRules = () => { can( [ - // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue + ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.Edit, @@ -811,7 +811,7 @@ export const projectMemberPermissions = buildMemberPermissionRules(); const buildViewerPermissionRules = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); - // not adding DescribeAndReadValue, because it's already covered by DescribeSecret and ReadValue + can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets); can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 9f3ec7111..72d36c26b 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -77,7 +77,7 @@ import { TSecretApprovalDetailsDTO, TStatusChangeDTO } from "./secret-approval-request-types"; -import { CheckForbiddenErrorSecretsSubject } from "../permission/permission-fns"; +import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns"; type TSecretApprovalRequestServiceFactoryDep = { permissionService: Pick; @@ -919,7 +919,7 @@ export const secretApprovalRequestServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath }); diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index 6afc6bca3..5fbb3f598 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -23,7 +23,10 @@ import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secre import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { TLicenseServiceFactory } from "../license/license-service"; -import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "../permission/permission-fns"; +import { + hasSecretReadValueOrDescribePermission, + throwIfMissingSecretReadValueOrDescribePermission +} from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionActions, @@ -103,7 +106,7 @@ export const secretSnapshotServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment, secretPath: path }); @@ -140,7 +143,7 @@ export const secretSnapshotServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment, secretPath: path }); @@ -187,12 +190,16 @@ export const secretSnapshotServiceFactory = ({ snapshotDetails = { ...encryptedSnapshotDetails, secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => { - const canReadValue = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment: encryptedSnapshotDetails.environment.slug, - secretPath: fullFolderPath, - secretName: el.key, - secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined - }); + const canReadValue = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment: encryptedSnapshotDetails.environment.slug, + secretPath: fullFolderPath, + secretName: el.key, + secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined + } + ); let secretValue = ""; if (canReadValue) { @@ -236,12 +243,16 @@ export const secretSnapshotServiceFactory = ({ key: botKey }); - const canReadValue = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment: encryptedSnapshotDetails.environment.slug, - secretPath: fullFolderPath, - secretName: secretKey, - secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined - }); + const canReadValue = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment: encryptedSnapshotDetails.environment.slug, + secretPath: fullFolderPath, + secretName: secretKey, + secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined + } + ); let secretValue = ""; diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index 3f7c55896..ad08e89d1 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; -import { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; +import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -96,7 +96,7 @@ export const integrationServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: sourceEnvironment, secretPath }); @@ -176,7 +176,7 @@ export const integrationServiceFactory = ({ const newSecretPath = secretPath || integration.secretPath; if (environment || secretPath) { - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: newEnvironment, secretPath: newSecretPath }); diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index b21cc99fe..dbef33c10 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -10,7 +10,7 @@ import { } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; +import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -765,7 +765,7 @@ export const projectServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); const project = await projectDAL.findProjectById(projectId); diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 27ed12c84..b8e8b2fa0 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -4,7 +4,10 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; +import { + hasSecretReadValueOrDescribePermission, + throwIfMissingSecretReadValueOrDescribePermission +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -94,7 +97,7 @@ export const secretImportServiceFactory = ({ ); // check if user has permission to import from target path - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment: data.environment, secretPath: data.path }); @@ -404,7 +407,7 @@ export const secretImportServiceFactory = ({ if (!secretImportDoc.isReplication) throw new BadRequestError({ message: "Import is not in replication mode" }); // check if user has permission to import from target path - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment: secretImportDoc.importEnv.slug, secretPath: secretImportDoc.importPath }); @@ -595,7 +598,7 @@ export const secretImportServiceFactory = ({ // so anything based on this order will also be in right position const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false }); const allowedImports = secretImports.filter((el) => - CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: el.importEnv.slug, secretPath: el.importPath }) @@ -645,7 +648,7 @@ export const secretImportServiceFactory = ({ secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, @@ -663,7 +666,7 @@ export const secretImportServiceFactory = ({ }); const allowedImports = secretImports.filter((el) => - CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: el.importEnv.slug, secretPath: el.importPath }) diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index d74ea120e..5c4a7e850 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; -import { CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; +import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionSecretActions, @@ -179,7 +179,7 @@ export const secretSyncServiceFactory = ({ ProjectPermissionSub.SecretSyncs ); - CheckForbiddenErrorSecretsSubject(projectPermission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(projectPermission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath }); @@ -267,7 +267,7 @@ export const secretSyncServiceFactory = ({ if (!updatedEnvironment || !updatedSecretPath) throw new BadRequestError({ message: "Must specify both source environment and secret path" }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: updatedEnvironment, secretPath: updatedSecretPath }); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 3b3a147bd..45c6493d6 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -10,7 +10,10 @@ import { TableName, TSecretsV2 } from "@app/db/schemas"; -import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; +import { + hasSecretReadValueOrDescribePermission, + throwIfMissingSecretReadValueOrDescribePermission +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -197,7 +200,7 @@ export const secretV2BridgeServiceFactory = ({ const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key); references.forEach((el) => { - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: el.environment, secretPath: el.secretPath, secretName: el.secretKey, @@ -542,14 +545,18 @@ export const secretV2BridgeServiceFactory = ({ }); } - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath, - secretName: inputSecret.secretName, - ...(tagsToCheck.length && { - secretTags: tagsToCheck.map((el) => el.slug) - }) - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath, + secretName: inputSecret.secretName, + ...(tagsToCheck.length && { + secretTags: tagsToCheck.map((el) => el.slug) + }) + } + ); return reshapeBridgeSecret( projectId, @@ -650,12 +657,16 @@ export const secretV2BridgeServiceFactory = ({ projectId }); - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath, - secretName: secretToDelete.key, - secretTags: secretToDelete.tags?.map((el) => el.slug) - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath, + secretName: secretToDelete.key, + secretTags: secretToDelete.tags?.map((el) => el.slug) + } + ); return reshapeBridgeSecret( projectId, @@ -698,7 +709,7 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -744,7 +755,7 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) return 0; @@ -779,37 +790,27 @@ export const secretV2BridgeServiceFactory = ({ }); const decryptedSecrets = secrets - .filter((el) => { - if ( - filterByAction === ProjectPermissionSecretActions.ReadValue || - filterByAction === ProjectPermissionSecretActions.DescribeSecret - ) { - return CheckCanSecretsSubject(projectPermission, filterByAction, { - environment: groupedFolderMappings[el.folderId][0].environment, - secretPath: groupedFolderMappings[el.folderId][0].path, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }); - } + .filter((el) => + hasSecretReadValueOrDescribePermission(projectPermission, filterByAction, { + environment: groupedFolderMappings[el.folderId][0].environment, + secretPath: groupedFolderMappings[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ) - return projectPermission.can( - filterByAction, - subject(ProjectPermissionSub.Secrets, { - environment: groupedFolderMappings[el.folderId][0].environment, - secretPath: groupedFolderMappings[el.folderId][0].path, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }) - ); - }) .map((secret) => { // Note(Daniel): This is only relevant if the filterAction isn't set to ReadValue. This is needed for the frontend. - const secretValueHidden = !CheckCanSecretsSubject(projectPermission, ProjectPermissionSecretActions.ReadValue, { - environment: groupedFolderMappings[secret.folderId][0].environment, - secretPath: groupedFolderMappings[secret.folderId][0].path, - secretName: secret.key, - secretTags: secret.tags.map((i) => i.slug) - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + projectPermission, + ProjectPermissionSecretActions.ReadValue, + { + environment: groupedFolderMappings[secret.folderId][0].environment, + secretPath: groupedFolderMappings[secret.folderId][0].path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + } + ); return reshapeBridgeSecret( projectId, @@ -855,7 +856,7 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); if (!isInternal) { - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -907,7 +908,7 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment, secretPath: path, secretTags: params.tagSlugs @@ -950,12 +951,16 @@ export const secretV2BridgeServiceFactory = ({ const decryptedSecrets = secrets .filter((el) => { - const canDescribeSecret = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { - environment, - secretPath: groupedPaths[el.folderId][0].path, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }); + const canDescribeSecret = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.DescribeSecret, + { + environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + } + ); if (!canDescribeSecret) { return false; @@ -964,7 +969,7 @@ export const secretV2BridgeServiceFactory = ({ if (viewSecretValue) { // Recursive secret, should be filtered out if (groupedPaths[el.folderId][0].path !== path) { - const canReadRecursiveSecretValue = CheckCanSecretsSubject( + const canReadRecursiveSecretValue = hasSecretReadValueOrDescribePermission( permission, ProjectPermissionSecretActions.ReadValue, { @@ -981,7 +986,7 @@ export const secretV2BridgeServiceFactory = ({ } if (throwOnMissingReadValuePermission) { - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath: groupedPaths[el.folderId][0].path, secretName: el.key, @@ -999,7 +1004,7 @@ export const secretV2BridgeServiceFactory = ({ const secretValueHidden = !viewSecretValue || - !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath: groupedPaths[secret.folderId][0].path, secretName: secret.key, @@ -1029,7 +1034,7 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, @@ -1074,19 +1079,27 @@ export const secretV2BridgeServiceFactory = ({ expandSecretReferences, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - const canDescribe = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }); + const canDescribe = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.DescribeSecret, + { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + } + ); - const canReadValue = CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }); + const canReadValue = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + } + ); return viewSecretValue ? canDescribe && canReadValue : canDescribe; } @@ -1128,7 +1141,7 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: folderWithPath.environmentSlug, secretPath: folderWithPath.path, secretName: secret.key, @@ -1240,7 +1253,7 @@ export const secretV2BridgeServiceFactory = ({ }) )); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment, secretPath: path, secretName, @@ -1255,7 +1268,7 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - return CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, @@ -1280,7 +1293,7 @@ export const secretV2BridgeServiceFactory = ({ decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined, hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { - return CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, @@ -1297,7 +1310,7 @@ export const secretV2BridgeServiceFactory = ({ if (viewSecretValue) { if ( - !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: importedSecret.environment, secretPath: importedSecrets[i].secretPath, secretName: importedSecret.key, @@ -1350,7 +1363,7 @@ export const secretV2BridgeServiceFactory = ({ if (viewSecretValue) { if ( - !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath: path, secretName, @@ -1523,12 +1536,16 @@ export const secretV2BridgeServiceFactory = ({ }); return newSecrets.map((el) => { - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath, - secretName: el.key, - secretTags: el.tags?.map((i) => i.slug) - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags?.map((i) => i.slug) + } + ); return reshapeBridgeSecret( projectId, @@ -1857,12 +1874,16 @@ export const secretV2BridgeServiceFactory = ({ ); return updatedSecrets.map((el) => { - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath: el.secretPath, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath: el.secretPath, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + } + ); return { ...reshapeBridgeSecret( @@ -1987,7 +2008,7 @@ export const secretV2BridgeServiceFactory = ({ const secretValueHidden = !secretToDeleteMatch || - !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath, secretName: el.key, @@ -2049,14 +2070,18 @@ export const secretV2BridgeServiceFactory = ({ sort: [["createdAt", "desc"]] }); return secretVersions.map((el) => { - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment: folder.environment.envSlug, - secretPath: folderWithPath.path, - secretName: el.key, - ...(el.tags?.length && { - secretTags: el.tags.map((tag) => tag.slug) - }) - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment: folder.environment.envSlug, + secretPath: folderWithPath.path, + secretName: el.key, + ...(el.tags?.length && { + secretTags: el.tags.map((tag) => tag.slug) + }) + } + ); return reshapeBridgeSecret( folder.projectId, @@ -2178,7 +2203,7 @@ export const secretV2BridgeServiceFactory = ({ sourceAction === ProjectPermissionSecretActions.DescribeSecret || sourceAction === ProjectPermissionSecretActions.ReadValue ) { - CheckForbiddenErrorSecretsSubject(permission, sourceAction, { + throwIfMissingSecretReadValueOrDescribePermission(permission, sourceAction, { environment: sourceEnvironment, secretPath: sourceSecretPath, secretName: secret.key, @@ -2517,7 +2542,7 @@ export const secretV2BridgeServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment, secretPath }); @@ -2541,7 +2566,7 @@ export const secretV2BridgeServiceFactory = ({ type: SecretType.Shared }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.DescribeSecret, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment, secretPath, secretName, @@ -2558,7 +2583,7 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretName, expandSecretTags) => - CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretName, @@ -2567,7 +2592,7 @@ export const secretV2BridgeServiceFactory = ({ }); if ( - !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath, secretName, diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index 510181d0c..473d1ed83 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -349,5 +349,5 @@ export type TGetSecretsRawByFolderMappingsDTO = { folderMappings: { folderId: string; path: string; environment: string }[]; userId: string; filters: TFindSecretsByFolderIdsFilter; - filterByAction?: ProjectPermissionSecretActions; + filterByAction?: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue; }; diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index ddaa054ab..aa8cec2bc 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -11,7 +11,7 @@ import { TSecretFolders, TSecrets } from "@app/db/schemas"; -import { CheckCanSecretsSubject } from "@app/ee/services/permission/permission-fns"; +import { hasSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; @@ -191,7 +191,7 @@ export const recursivelyGetSecretPaths = ({ // Filter out paths that the user does not have permission to access, and paths that are not in the current path const allowedPaths = paths.filter( (folder) => - CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath: folder.path }) && folder.path.startsWith(currentPath === "/" ? "" : currentPath) diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index e3eb75bda..3593adf88 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -13,7 +13,10 @@ import { SecretType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { CheckCanSecretsSubject, CheckForbiddenErrorSecretsSubject } from "@app/ee/services/permission/permission-fns"; +import { + hasSecretReadValueOrDescribePermission, + throwIfMissingSecretReadValueOrDescribePermission +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -453,10 +456,14 @@ export const secretServiceFactory = ({ }); } - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath: path - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath: path + } + ); return { ...updatedSecret[0], @@ -560,10 +567,14 @@ export const secretServiceFactory = ({ }); } - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath: path - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath: path + } + ); return { ...deletedSecret[0], @@ -620,7 +631,7 @@ export const secretServiceFactory = ({ paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p })); } else { - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath: path }); @@ -645,7 +656,7 @@ export const secretServiceFactory = ({ // if its service token allow full access over imported one actor === ActorType.SERVICE ? true - : CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + : hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: importEnv.slug, secretPath: importPath }) @@ -699,7 +710,7 @@ export const secretServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - CheckForbiddenErrorSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment, secretPath: path }); @@ -750,7 +761,7 @@ export const secretServiceFactory = ({ // if its service token allow full access over imported one actor === ActorType.SERVICE ? true - : CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { + : hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { environment: importEnv.slug, secretPath: importPath }) @@ -969,10 +980,14 @@ export const secretServiceFactory = ({ secretVersionTagDAL }); - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath: path - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath: path + } + ); return updatedSecrets.map((secret) => ({ ...secret, @@ -1063,10 +1078,14 @@ export const secretServiceFactory = ({ }); } } - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath: path - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath: path + } + ); return secrets.map((secret) => ({ ...secret, @@ -1257,7 +1276,7 @@ export const secretServiceFactory = ({ action === ProjectPermissionSecretActions.DescribeSecret || action === ProjectPermissionSecretActions.ReadValue ) { - return CheckCanSecretsSubject(entityPermission.permission, action, { + return hasSecretReadValueOrDescribePermission(entityPermission.permission, action, { environment, secretPath, secretName, @@ -2429,14 +2448,18 @@ export const secretServiceFactory = ({ key: botKey }); - const secretValueHidden = !CheckCanSecretsSubject(permission, ProjectPermissionSecretActions.ReadValue, { - environment: folder.environment.envSlug, - secretPath: folderWithPath.path, - secretName: secretKey, - ...(el.tags?.length && { - secretTags: el.tags.map((tag) => tag.slug) - }) - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment: folder.environment.envSlug, + secretPath: folderWithPath.path, + secretName: secretKey, + ...(el.tags?.length && { + secretTags: el.tags.map((tag) => tag.slug) + }) + } + ); return decryptSecretRaw( { @@ -2839,7 +2862,7 @@ export const secretServiceFactory = ({ sourceAction === ProjectPermissionSecretActions.ReadValue || sourceAction === ProjectPermissionSecretActions.DescribeSecret ) { - CheckForbiddenErrorSecretsSubject(permission, sourceAction, { + throwIfMissingSecretReadValueOrDescribePermission(permission, sourceAction, { environment: sourceEnvironment, secretPath: sourceSecretPath }); diff --git a/frontend/src/lib/fn/permission.ts b/frontend/src/lib/fn/permission.ts index f2077d76b..c3937a0ff 100644 --- a/frontend/src/lib/fn/permission.ts +++ b/frontend/src/lib/fn/permission.ts @@ -7,7 +7,7 @@ import { SecretSubjectFields } from "@app/context/ProjectPermissionContext/types"; -export function secretsPermissionCan( +export function hasSecretReadValueOrDescribePermission( permission: MongoAbility, action: Extract< ProjectPermissionSecretActions, diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index 9f13b53a8..a47a64614 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -29,11 +29,11 @@ const GeneralPolicyActionSchema = z.object({ const SecretPolicyActionSchema = z.object({ [ProjectPermissionSecretActions.DescribeAndReadValue]: z.boolean().optional(), // existing read, gives both describe and read value - [ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(), // describe secret, cannot read value - [ProjectPermissionSecretActions.ReadValue]: z.boolean().optional(), // read value - [ProjectPermissionSecretActions.Edit]: z.boolean().optional(), // edit secret - [ProjectPermissionSecretActions.Delete]: z.boolean().optional(), // delete secret - [ProjectPermissionSecretActions.Create]: z.boolean().optional() // create secret + [ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(), + [ProjectPermissionSecretActions.ReadValue]: z.boolean().optional(), + [ProjectPermissionSecretActions.Edit]: z.boolean().optional(), + [ProjectPermissionSecretActions.Delete]: z.boolean().optional(), + [ProjectPermissionSecretActions.Create]: z.boolean().optional() }); const CmekPolicyActionSchema = z.object({ diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx index 536f75542..ce2e43f67 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretRenameRow.tsx @@ -15,7 +15,7 @@ import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionCo import { useToggle } from "@app/hooks"; import { useUpdateSecretV3 } from "@app/hooks/api"; import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types"; -import { secretsPermissionCan } from "@app/lib/fn/permission"; +import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; enum SecretActionType { Created = "created", @@ -52,7 +52,7 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath } secretTags: (secretDetails?.tags || []).map((i) => i.slug) }); const isSecretInEnvReadOnly = - secretsPermissionCan( + hasSecretReadValueOrDescribePermission( permission, ProjectPermissionSecretActions.DescribeSecret, secretPermissionSubject diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index 4dba1c2a8..e6fc5eadf 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -38,7 +38,7 @@ import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard"; import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { ProjectType } from "@app/hooks/api/workspace/types"; -import { secretsPermissionCan } from "@app/lib/fn/permission"; +import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { SecretTableResourceCount } from "../OverviewPage/components/SecretTableResourceCount"; import { SecretV2MigrationSection } from "../OverviewPage/components/SecretV2MigrationSection"; @@ -105,7 +105,7 @@ const Page = () => { const projectSlug = currentWorkspace?.slug || ""; const secretPath = (routerQueryParams.secretPath as string) || "/"; - const canReadSecret = secretsPermissionCan( + const canReadSecret = hasSecretReadValueOrDescribePermission( permission, ProjectPermissionSecretActions.DescribeSecret, { @@ -116,7 +116,7 @@ const Page = () => { } ); - const canReadSecretValue = secretsPermissionCan( + const canReadSecretValue = hasSecretReadValueOrDescribePermission( permission, ProjectPermissionSecretActions.ReadValue, { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index 3ad7159d7..410d49e5f 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -57,7 +57,7 @@ import { ActorType } from "@app/hooks/api/auditLogs/enums"; import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries"; import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types"; import { ProjectType } from "@app/hooks/api/workspace/types"; -import { secretsPermissionCan } from "@app/lib/fn/permission"; +import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { CreateReminderForm } from "./CreateReminderForm"; import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; @@ -141,7 +141,7 @@ export const SecretDetailSidebar = ({ }) ); - const cannotReadSecretValue = !secretsPermissionCan( + const cannotReadSecretValue = !hasSecretReadValueOrDescribePermission( permission, ProjectPermissionSecretActions.ReadValue, { @@ -153,12 +153,16 @@ export const SecretDetailSidebar = ({ ); const isReadOnly = - secretsPermissionCan(permission, ProjectPermissionSecretActions.DescribeSecret, { - environment, - secretPath, - secretName: secretKey, - secretTags: selectTagSlugs - }) && + hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.DescribeSecret, + { + environment, + secretPath, + secretName: secretKey, + secretTags: selectTagSlugs + } + ) && cannotEditSecret && cannotReadSecretValue; @@ -361,11 +365,11 @@ export const SecretDetailSidebar = ({ >
Date: Tue, 11 Mar 2025 00:33:11 +0400 Subject: [PATCH 10/15] Update go.sum --- cli/go.sum | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/cli/go.sum b/cli/go.sum index d209fd1ca..b72858e9d 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -367,8 +367,6 @@ github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= -github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= -github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -427,8 +425,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= @@ -870,4 +868,4 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= \ No newline at end of file From b9b76579ac14a3b63cdb2b0e0bb255d138073f61 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 11 Mar 2025 02:07:38 +0400 Subject: [PATCH 11/15] requested changes --- .../src/ee/routes/v1/user-additional-privilege-router.ts | 8 ++++++-- .../v2/identity-project-additional-privilege-router.ts | 8 ++++++-- backend/src/ee/routes/v2/project-role-router.ts | 4 +++- backend/src/ee/services/permission/permission-fns.ts | 5 ++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/backend/src/ee/routes/v1/user-additional-privilege-router.ts b/backend/src/ee/routes/v1/user-additional-privilege-router.ts index de37a4cde..f8b4e922c 100644 --- a/backend/src/ee/routes/v1/user-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts @@ -2,6 +2,7 @@ import slugify from "@sindresorhus/slugify"; import ms from "ms"; import { z } from "zod"; +import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; @@ -23,7 +24,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr body: z.object({ projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId), slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), - permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions), + permissions: ProjectPermissionV2Schema.array() + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions) + .refine(checkForInvalidPermissionCombination), type: z.discriminatedUnion("isTemporary", [ z.object({ isTemporary: z.literal(false) @@ -81,7 +84,8 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug), permissions: ProjectPermissionV2Schema.array() .optional() - .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions), + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions) + .refine(checkForInvalidPermissionCombination), type: z.discriminatedUnion("isTemporary", [ z.object({ isTemporary: z.literal(false).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary) }), z.object({ diff --git a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts index d9c3a05b5..7cc9155cd 100644 --- a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts @@ -3,6 +3,7 @@ import ms from "ms"; import { z } from "zod"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types"; +import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -30,7 +31,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId), projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId), slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug), - permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission), + permissions: ProjectPermissionV2Schema.array() + .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission) + .refine(checkForInvalidPermissionCombination), type: z.discriminatedUnion("isTemporary", [ z.object({ isTemporary: z.literal(false) @@ -94,7 +97,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug), permissions: ProjectPermissionV2Schema.array() .optional() - .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission), + .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission) + .refine(checkForInvalidPermissionCombination), type: z.discriminatedUnion("isTemporary", [ z.object({ isTemporary: z.literal(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary) }), z.object({ diff --git a/backend/src/ee/routes/v2/project-role-router.ts b/backend/src/ee/routes/v2/project-role-router.ts index c3b3c11f2..0bb83b8d4 100644 --- a/backend/src/ee/routes/v2/project-role-router.ts +++ b/backend/src/ee/routes/v2/project-role-router.ts @@ -38,7 +38,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { .describe(PROJECT_ROLE.CREATE.slug), name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name), description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description), - permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions) + permissions: ProjectPermissionV2Schema.array() + .describe(PROJECT_ROLE.CREATE.permissions) + .refine(checkForInvalidPermissionCombination) }), response: { 200: z.object({ diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index ecc8b676e..9397df939 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -81,7 +81,8 @@ export function checkForInvalidPermissionCombination(permissions: z.infer Date: Tue, 11 Mar 2025 04:52:12 +0400 Subject: [PATCH 12/15] requested changes --- .../ee/services/permission/permission-fns.ts | 12 +- .../src/server/routes/v1/dashboard-router.ts | 45 ++++ .../secret-v2-bridge-service.ts | 90 +++++++- .../secret-v2-bridge-types.ts | 7 + backend/src/services/secret/secret-service.ts | 37 ++- backend/src/services/secret/secret-types.ts | 7 + frontend/src/hooks/api/dashboard/index.ts | 1 + frontend/src/hooks/api/dashboard/queries.tsx | 57 ++++- frontend/src/hooks/api/dashboard/types.ts | 10 + .../SecretOverviewTableRow/SecretEditRow.tsx | 89 +++---- .../SecretDropzone/CopySecretsFromBoard.tsx | 60 ++--- .../SecretListView/SecretDetailSidebar.tsx | 217 +++++++++--------- 12 files changed, 441 insertions(+), 191 deletions(-) diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index 9397df939..cccadb86e 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -23,12 +23,6 @@ export function throwIfMissingSecretReadValueOrDescribePermission( subjectFields?: SecretSubjectFields ) { try { - if (subjectFields) { - ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields)); - } else { - ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets); - } - } catch { if (subjectFields) { ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionSecretActions.DescribeAndReadValue, @@ -40,6 +34,12 @@ export function throwIfMissingSecretReadValueOrDescribePermission( ProjectPermissionSub.Secrets ); } + } catch { + if (subjectFields) { + ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields)); + } else { + ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets); + } } } diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 27ba3f8a3..392fcf5d9 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -834,6 +834,51 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/accessible-secrets", + config: { + rateLimit: secretsLimit + }, + schema: { + querystring: z.object({ + projectId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().default("/").transform(removeTrailingSlash), + filterByAction: z + .enum([ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue]) + .default(ProjectPermissionSecretActions.ReadValue) + }), + response: { + 200: z.object({ + secrets: secretRawSchema + .extend({ + secretPath: z.string().optional() + }) + .array() + .optional() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { projectId, environment, secretPath, filterByAction } = req.query; + + const { secrets } = await server.services.secret.getAccessibleSecrets({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + environment, + secretPath, + projectId, + filterByAction + }); + + return { secrets }; + } + }); + server.route({ method: "GET", url: "/secrets-by-keys", diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 45c6493d6..c66fdddf1 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -60,6 +60,7 @@ import { TCreateSecretDTO, TDeleteManySecretDTO, TDeleteSecretDTO, + TGetAccessibleSecretsDTO, TGetASecretDTO, TGetSecretReferencesTreeDTO, TGetSecretsDTO, @@ -200,7 +201,7 @@ export const secretV2BridgeServiceFactory = ({ const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key); references.forEach((el) => { - throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment: el.environment, secretPath: el.secretPath, secretName: el.secretKey, @@ -1291,7 +1292,7 @@ export const secretV2BridgeServiceFactory = ({ folderDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), - expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined, + expandSecretReferences: shouldExpandSecretReferences && viewSecretValue ? expandSecretReferences : undefined, hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment: expandEnvironment, @@ -1347,7 +1348,7 @@ export const secretV2BridgeServiceFactory = ({ let secretValue = secret.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() : ""; - if (shouldExpandSecretReferences && secretValue) { + if (shouldExpandSecretReferences && secretValue && viewSecretValue) { // eslint-disable-next-line const expandedSecretValue = await expandSecretReferences({ environment, @@ -2613,6 +2614,86 @@ export const secretV2BridgeServiceFactory = ({ return { tree: stackTrace, value: expandedValue }; }; + const getAccessibleSecrets = async ({ + projectId, + secretPath, + environment, + filterByAction, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TGetAccessibleSecretsDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath + }); + + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + if (!folder) return { secrets: [] }; + + const secrets = await secretDAL.findByFolderIds([folder.id]); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const decryptedSecrets = secrets + .filter((el) => { + if ( + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ) { + return false; + } + + if (filterByAction === ProjectPermissionSecretActions.ReadValue) { + return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }); + } + + return true; + }) + .map((secret) => { + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...secret, + value: secret.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() + : "", + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + }, + false + ); + }); + + return { + secrets: decryptedSecrets + }; + }; + return { createSecret, deleteSecret, @@ -2630,6 +2711,7 @@ export const secretV2BridgeServiceFactory = ({ getSecretsMultiEnv, getSecretReferenceTree, getSecretsByFolderMappings, - getSecretById + getSecretById, + getAccessibleSecrets }; }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index 473d1ed83..7f415bff8 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -351,3 +351,10 @@ export type TGetSecretsRawByFolderMappingsDTO = { filters: TFindSecretsByFolderIdsFilter; filterByAction?: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue; }; + +export type TGetAccessibleSecretsDTO = { + environment: string; + projectId: string; + secretPath: string; + filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue; +} & TProjectPermission; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 3593adf88..13a5c9736 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -81,6 +81,7 @@ import { TDeleteManySecretRawDTO, TDeleteSecretDTO, TDeleteSecretRawDTO, + TGetAccessibleSecretsDTO, TGetASecretByIdRawDTO, TGetASecretDTO, TGetASecretRawDTO, @@ -1312,6 +1313,39 @@ export const secretServiceFactory = ({ return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess }; }; + const getAccessibleSecrets = async ({ + projectId, + secretPath, + actor, + actorId, + actorOrgId, + actorAuthMethod, + environment, + filterByAction + }: TGetAccessibleSecretsDTO) => { + const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); + + if (!shouldUseSecretV2Bridge) { + throw new BadRequestError({ + message: "Project version does not support this endpoint.", + name: "ProjectVersionNotSupported" + }); + } + + const secrets = await secretV2BridgeService.getAccessibleSecrets({ + projectId, + secretPath, + environment, + filterByAction, + actor, + actorId, + actorOrgId, + actorAuthMethod + }); + + return secrets; + }; + const getSecretsRaw = async ({ projectId, path, @@ -3261,6 +3295,7 @@ export const secretServiceFactory = ({ getSecretReferenceTree, getSecretsRawByFolderMappings, getSecretAccessList, - getSecretByIdRaw + getSecretByIdRaw, + getAccessibleSecrets }; }; diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index c7b8a067d..f42f70b78 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -20,6 +20,7 @@ import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge- import { SecretUpdateMode } from "../secret-v2-bridge/secret-v2-bridge-types"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; +import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; type TPartialSecret = Pick; @@ -180,6 +181,12 @@ export enum SecretsOrderBy { Name = "name" // "key" for secrets but using name for use across resources } +export type TGetAccessibleSecretsDTO = { + secretPath: string; + environment: string; + filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue; +} & TProjectPermission; + export type TGetSecretsRawDTO = { expandSecretReferences?: boolean; path: string; diff --git a/frontend/src/hooks/api/dashboard/index.ts b/frontend/src/hooks/api/dashboard/index.ts index 83206bdf8..bbd70e306 100644 --- a/frontend/src/hooks/api/dashboard/index.ts +++ b/frontend/src/hooks/api/dashboard/index.ts @@ -1,4 +1,5 @@ export { + useGetAccessibleSecrets, useGetProjectSecretsDetails, useGetProjectSecretsOverview, useGetProjectSecretsQuickSearch diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx index f704081a9..049238202 100644 --- a/frontend/src/hooks/api/dashboard/queries.tsx +++ b/frontend/src/hooks/api/dashboard/queries.tsx @@ -11,6 +11,7 @@ import { DashboardSecretsOrderBy, TDashboardProjectSecretsQuickSearch, TDashboardProjectSecretsQuickSearchResponse, + TGetAccessibleSecretsDTO, TGetDashboardProjectSecretsByKeys, TGetDashboardProjectSecretsDetailsDTO, TGetDashboardProjectSecretsOverviewDTO, @@ -20,6 +21,8 @@ import { OrderByDirection } from "@app/hooks/api/generic/types"; import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries"; import { groupBy, unique } from "@app/lib/fn/array"; +import { SecretV3Raw } from "../types"; + export const dashboardKeys = { all: () => ["dashboard"] as const, getDashboardSecrets: ({ @@ -58,7 +61,14 @@ export const dashboardKeys = { ...dashboardKeys.getDashboardSecrets({ projectId, secretPath }), "quick-search", params - ] as const + ] as const, + getAccessibleSecrets: ({ + projectId, + secretPath, + environment, + filterByAction + }: TGetAccessibleSecretsDTO) => + [...dashboardKeys.all(), { projectId, secretPath, environment, filterByAction }] as const }; export const fetchProjectSecretsOverview = async ({ @@ -295,6 +305,22 @@ export const fetchProjectSecretsQuickSearch = async ({ return data; }; +const fetchAccessibleSecrets = async ({ + projectId, + secretPath, + environment, + filterByAction +}: TGetAccessibleSecretsDTO) => { + const { data } = await apiRequest.get<{ secrets: SecretV3Raw[] }>( + "/api/v1/dashboard/accessible-secrets", + { + params: { projectId, secretPath, environment, filterByAction } + } + ); + + return data.secrets; +}; + export const useGetProjectSecretsQuickSearch = ( { projectId, @@ -357,3 +383,32 @@ export const useGetProjectSecretsQuickSearch = ( placeholderData: (previousData) => previousData }); }; + +export const useGetAccessibleSecrets = ({ + projectId, + secretPath, + environment, + filterByAction, + options +}: TGetAccessibleSecretsDTO & { + options?: Omit< + UseQueryOptions< + SecretV3Raw[], + unknown, + SecretV3Raw[], + ReturnType + >, + "queryKey" | "queryFn" + >; +}) => { + return useQuery({ + ...options, + queryKey: dashboardKeys.getAccessibleSecrets({ + projectId, + secretPath, + environment, + filterByAction + }), + queryFn: () => fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction }) + }); +}; diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index 4808b463c..9540c4ae6 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -1,3 +1,4 @@ +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { TSecretFolder } from "@app/hooks/api/secretFolders/types"; @@ -101,3 +102,12 @@ export type TGetDashboardProjectSecretsByKeys = { environment: string; keys: string[]; }; + +export type TGetAccessibleSecretsDTO = { + projectId: string; + secretPath: string; + environment: string; + filterByAction: + | ProjectPermissionSecretActions.DescribeSecret + | ProjectPermissionSecretActions.ReadValue; +}; diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx index 4887d4fd7..705d7e177 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -27,9 +27,11 @@ import { } from "@app/components/v2"; import { Blur } from "@app/components/v2/Blur"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { useToggle } from "@app/hooks"; import { SecretType } from "@app/hooks/api/types"; +import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; type Props = { defaultValue?: string | null; @@ -79,6 +81,9 @@ export const SecretEditRow = ({ value: defaultValue || null } }); + + const { permission } = useProjectPermission(); + const [isDeleting, setIsDeleting] = useToggle(); const [isModalOpen, setIsModalOpen] = useState(false); @@ -120,6 +125,11 @@ export const SecretEditRow = ({ reset({ value }); }; + const canReadSecretValue = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue + ); + const handleDeleteSecret = useCallback(async () => { setIsDeleting.on(); setIsModalOpen(false); @@ -229,48 +239,43 @@ export const SecretEditRow = ({
- - {(isAllowed) => ( -
- - -
- - - - - -
-
- e.preventDefault()} // prevents secret input from displaying value on open + +
+ + +
+ - - - -
- )} - + + + + +
+ + e.preventDefault()} // prevents secret input from displaying value on open + > + + +
+
+ 1 ? val.slice(0, -1) : val ), secrets: z - .object({ key: z.string(), value: z.string().optional() }) + .object({ secretKey: z.string(), secretValue: z.string().optional() }) .array() .min(1, "Select one or more secrets to copy") }); @@ -78,34 +79,38 @@ export const CopySecretsFromBoard = ({ const selectedEnvSlug = watch("environment"); const [debouncedEnvCopySecretPath] = useDebounce(envCopySecPath); - const { data: secrets, isPending: isSecretsLoading } = useGetProjectSecrets({ - workspaceId, - environment: selectedEnvSlug.slug, - secretPath: debouncedEnvCopySecretPath, - options: { - enabled: - Boolean(workspaceId) && - Boolean(selectedEnvSlug) && - Boolean(debouncedEnvCopySecretPath) && - isOpen - } - }); + const { data: accessibleSecrets, isPending: isAccessibleSecretsLoading } = + useGetAccessibleSecrets({ + projectId: workspaceId, + secretPath: debouncedEnvCopySecretPath, + environment: selectedEnvSlug.slug, + filterByAction: shouldIncludeValues + ? ProjectPermissionSecretActions.ReadValue + : ProjectPermissionSecretActions.DescribeSecret, + options: { + enabled: + Boolean(workspaceId) && + Boolean(selectedEnvSlug) && + Boolean(debouncedEnvCopySecretPath) && + isOpen + } + }); useEffect(() => { setValue("secrets", []); }, [debouncedEnvCopySecretPath, selectedEnvSlug]); const handleSecSelectAll = () => { - if (secrets) { - setValue("secrets", secrets, { shouldDirty: true }); + if (accessibleSecrets) { + setValue("secrets", accessibleSecrets, { shouldDirty: true }); } }; const handleFormSubmit = async (data: TFormSchema) => { const secretsToBePulled: Record = {}; - data.secrets.forEach(({ key, value }) => { - secretsToBePulled[key] = { - value: (shouldIncludeValues && value) || "", + data.secrets.forEach(({ secretKey, secretValue }) => { + secretsToBePulled[secretKey] = { + value: (shouldIncludeValues && secretValue) || "", comments: [""] }; }); @@ -202,19 +207,19 @@ export const CopySecretsFromBoard = ({ option.key} - getOptionLabel={(option) => option.key} + getOptionValue={(option) => option.secretKey} + getOptionLabel={(option) => option.secretKey} /> )} @@ -235,7 +240,10 @@ export const CopySecretsFromBoard = ({ setShouldIncludeValues(isChecked as boolean)} + onCheckedChange={(isChecked) => { + setValue("secrets", []); + setShouldIncludeValues(isChecked as boolean); + }} > Include secret values diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index 410d49e5f..641a4a723 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -13,8 +13,8 @@ import { faShare, faTag, faTrash, - faUser, - faTriangleExclamation + faTriangleExclamation, + faUser } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -728,8 +728,8 @@ export const SecretDetailSidebar = ({
Version History
{secretVersion?.map( - ({ createdAt, secretValue, version, id, secretValueHidden, actor }, index) => ( -
+ ({ createdAt, secretValue, secretValueHidden, version, id, actor }) => ( +
@@ -739,72 +739,50 @@ export const SecretDetailSidebar = ({
{format(new Date(createdAt), "Pp")}
-
{format(new Date(createdAt), "Pp")}
-
-
-
-
-
-
- {actor && ( -
-
- Modified by: - - {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} -
- onModifyHistoryClick( - actor.actorId, - actor.actorType, - actor.membershipId - ) - } - className="cursor-pointer" +
+
+
+
+
+ {actor && ( +
+
+ Modified by: + - -
- + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} +
+ onModifyHistoryClick( + actor.actorId, + actor.actorType, + actor.membershipId + ) + } + className="cursor-pointer" + > + +
+ +
-
- )} -
-
- Value: -
-
-
- - + -
- - {secretValueHidden ? "******" : secretValue?.replace(/./g, "*")} - +
+ + {secretValueHidden ? "******" : secretValue?.replace(/./g, "*")} + - + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.currentTarget + .closest(".group") + ?.classList.add("show-value"); + } + }} + > + + + +
From 497761a0e5d24c47face9c416caaaa2d86fe6a56 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 11 Mar 2025 05:44:28 +0400 Subject: [PATCH 13/15] fix: missing permision check --- backend/src/server/routes/v1/dashboard-router.ts | 3 ++- .../secret-v2-bridge/secret-v2-bridge-service.ts | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 392fcf5d9..db61594a2 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -853,7 +853,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { 200: z.object({ secrets: secretRawSchema .extend({ - secretPath: z.string().optional() + secretPath: z.string().optional(), + secretValueHidden: z.boolean() }) .array() .optional() diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index c66fdddf1..09b3a50cb 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -2672,6 +2672,15 @@ export const secretV2BridgeServiceFactory = ({ return true; }) .map((secret) => { + const secretValueHidden = + filterByAction === ProjectPermissionSecretActions.DescribeSecret && + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }); + return reshapeBridgeSecret( projectId, environment, @@ -2685,7 +2694,7 @@ export const secretV2BridgeServiceFactory = ({ ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() : "" }, - false + secretValueHidden ); }); From ad1dd55b8bf1cbff714132be1e9e2376df0dee18 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 11 Mar 2025 06:01:21 +0400 Subject: [PATCH 14/15] chore: requested changes --- frontend/src/hooks/api/dashboard/queries.tsx | 6 +++++- frontend/src/hooks/api/reactQuery.tsx | 6 +----- frontend/src/lib/fn/string.ts | 4 ++++ .../SecretListView/SecretDetailSidebar.tsx | 12 +++++------- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx index 049238202..1fb456325 100644 --- a/frontend/src/hooks/api/dashboard/queries.tsx +++ b/frontend/src/hooks/api/dashboard/queries.tsx @@ -68,7 +68,11 @@ export const dashboardKeys = { environment, filterByAction }: TGetAccessibleSecretsDTO) => - [...dashboardKeys.all(), { projectId, secretPath, environment, filterByAction }] as const + [ + ...dashboardKeys.all(), + "accessible-secrets", + { projectId, secretPath, environment, filterByAction } + ] as const }; export const fetchProjectSecretsOverview = async ({ diff --git a/frontend/src/hooks/api/reactQuery.tsx b/frontend/src/hooks/api/reactQuery.tsx index be9dadbec..cd8622976 100644 --- a/frontend/src/hooks/api/reactQuery.tsx +++ b/frontend/src/hooks/api/reactQuery.tsx @@ -10,18 +10,14 @@ import { formatedConditionsOperatorNames, PermissionConditionOperators } from "@app/context/ProjectPermissionContext/types"; +import { camelCaseToSpaces } from "@app/lib/fn/string"; import { ApiErrorTypes, TApiErrors } from "./types"; - // this is saved in react-query cache export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"]; export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"]; export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"]; -const camelCaseToSpaces = (input: string) => { - return input.replace(/([a-z])([A-Z])/g, "$1 $2"); -}; - export const queryClient = new QueryClient({ mutationCache: new MutationCache({ onError: (error) => { diff --git a/frontend/src/lib/fn/string.ts b/frontend/src/lib/fn/string.ts index 9d3d01cc5..f4c4a43db 100644 --- a/frontend/src/lib/fn/string.ts +++ b/frontend/src/lib/fn/string.ts @@ -7,3 +7,7 @@ export const formatReservedPaths = (secretPath: string) => { } return secretPath; }; + +export const camelCaseToSpaces = (input: string) => { + return input.replace(/([a-z])([A-Z])/g, "$1 $2"); +}; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index 641a4a723..382b574c8 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -61,6 +61,7 @@ import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { CreateReminderForm } from "./CreateReminderForm"; import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; +import { camelCaseToSpaces } from "@app/lib/fn/string"; type Props = { isOpen?: boolean; @@ -729,8 +730,8 @@ export const SecretDetailSidebar = ({
{secretVersion?.map( ({ createdAt, secretValue, secretValueHidden, version, id, actor }) => ( -
-
+
+
@@ -936,12 +937,9 @@ export const SecretDetailSidebar = ({
- action.charAt(0).toUpperCase() + action.slice(1).toLowerCase() - ) + .map((action) => camelCaseToSpaces(action)) .join(", ")} - className="z-[100]" + className="z-[100] capitalize" > Date: Tue, 11 Mar 2025 07:11:14 +0400 Subject: [PATCH 15/15] chore: rolled back service token permission changes --- .../services/permission/project-permission.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 43f832d22..c622e65a7 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -856,14 +856,12 @@ export const buildServiceTokenProjectPermission = ( ) => { const canWrite = permission.includes("write"); const canRead = permission.includes("read"); - const { can, build } = new AbilityBuilder>(createMongoAbility); scopes.forEach(({ secretPath, environment }) => { [ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretFolders].forEach( (subject) => { if (canWrite) { can(ProjectPermissionActions.Edit, subject, { - // TODO: @Akhi // @ts-expect-error type secretPath: { $glob: secretPath }, environment @@ -879,27 +877,13 @@ export const buildServiceTokenProjectPermission = ( environment }); } - if (canRead && subject !== ProjectPermissionSub.Secrets) { + if (canRead) { can(ProjectPermissionActions.Read, subject, { // @ts-expect-error type secretPath: { $glob: secretPath }, environment }); } - - if (subject === ProjectPermissionSub.Secrets && canRead) { - // @ts-expect-error type - can(ProjectPermissionSecretActions.ReadValue, subject as ProjectPermissionSub.Secrets, { - secretPath: { $glob: secretPath }, - environment - }); - - // @ts-expect-error type - can(ProjectPermissionSecretActions.DescribeSecret, subject as ProjectPermissionSub.Secrets, { - secretPath: { $glob: secretPath }, - environment - }); - } } ); });