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..bc25bf165 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -10,7 +10,7 @@ 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 +250,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 +283,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 +302,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/snapshot-router.ts b/backend/src/ee/routes/v1/snapshot-router.ts index fc2d25712..64471393b 100644 --- a/backend/src/ee/routes/v1/snapshot-router.ts +++ b/backend/src/ee/routes/v1/snapshot-router.ts @@ -31,6 +31,7 @@ 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, @@ -55,6 +56,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 c270b4a93..9347d69e7 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -442,7 +442,7 @@ export const ProjectPermissionV1Schema = 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: SecretConditionV1Schema.describe( @@ -947,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 @@ -989,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 5abd47d75..5430558a9 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 @@ -111,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; 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-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index b7a972d02..35023028f 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -38,6 +38,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; @@ -184,7 +185,7 @@ export const secretSnapshotServiceFactory = ({ snapshotDetails = { ...encryptedSnapshotDetails, secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => { - ForbiddenError.from(permission).throwUnlessCan( + const canReadValue = permission.can( ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: encryptedSnapshotDetails.environment.slug, @@ -194,12 +195,20 @@ export const secretSnapshotServiceFactory = ({ }) ); + let secretValue = ""; + if (canReadValue) { + secretValue = el.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() + : ""; + } else { + secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK; + } + return { ...el, secretKey: el.key, - secretValue: el.encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() - : "", + secretValueHidden: !canReadValue, + secretValue, secretComment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" @@ -228,7 +237,7 @@ export const secretSnapshotServiceFactory = ({ key: botKey }); - ForbiddenError.from(permission).throwUnlessCan( + const canReadValue = permission.can( ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment: encryptedSnapshotDetails.environment.slug, @@ -238,15 +247,24 @@ export const secretSnapshotServiceFactory = ({ }) ); - return { - ...el, - secretKey, - secretValue: decryptSymmetric128BitHexKeyUTF8({ + 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({ diff --git a/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts b/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts index cf8d59d8c..834a8990a 100644 --- a/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts +++ b/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + import { SshCertificatesSchema } from "@app/db/schemas"; export const sanitizedSshCertificate = SshCertificatesSchema.pick({ diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 4d645ac4b..024e44224 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"; @@ -232,3 +233,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 24dee9c87..7fe602aea 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -15,7 +15,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"; @@ -119,14 +119,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { 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(), @@ -416,14 +409,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { 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(), @@ -605,24 +591,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, - viewSecretValue: req.query.viewSecretValue, - 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, @@ -703,14 +690,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { .extend({ 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() @@ -868,7 +848,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { environment: z.string().trim(), secretPath: z.string().trim().default("/").transform(removeTrailingSlash), keys: z.string().trim().transform(decodeURIComponent), - viewSecretValue: booleanSchema.default(true) + viewSecretValue: booleanSchema.default(false) }), response: { 200: z.object({ @@ -877,14 +857,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { 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() diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 8262a5eab..9e72a4094 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -23,7 +23,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(), @@ -72,7 +72,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { body: z.object({ projectSlug: z.string().trim().describe(SECRETS.ATTACH_TAGS.projectSlug), environment: z.string().trim().describe(SECRETS.ATTACH_TAGS.environment), - viewSecretValue: convertStringBoolean(true).describe(SECRETS.ATTACH_TAGS.viewSecretValue), secretPath: z .string() .trim() @@ -84,17 +83,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() + }) }) } }, @@ -148,13 +139,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() }) }) } @@ -274,14 +259,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { 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 @@ -292,8 +270,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: secretRawSchema .omit({ createdAt: true, updatedAt: true }) .extend({ - // No `secretValueHidden` on imports, because imported secrets that the user doesn't have read value permission on, will be filtered out. - // Therefore all returned secret imports will have the value present. + secretValueHidden: z.boolean(), secretMetadata: ResourceMetadataSchema.optional() }) .array() @@ -414,14 +391,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { 200: z.object({ secret: secretRawSchema.extend({ secretValueHidden: z.boolean(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional(), + tags: SanitizedTagSchema.array().optional(), secretMetadata: ResourceMetadataSchema.optional() }) }) @@ -845,13 +815,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 @@ -945,7 +909,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { workspaceId: z.string().trim(), environment: z.string().trim(), secretPath: z.string().trim().default("/").transform(removeTrailingSlash), - viewSecretValue: convertStringBoolean(true), type: z.nativeEnum(SecretType).default(SecretType.Shared), version: z.coerce.number().optional(), include_imports: convertStringBoolean() diff --git a/backend/src/services/external-migration/external-migration-fns.ts b/backend/src/services/external-migration/external-migration-fns.ts index cd0e1c858..fe0d49ca8 100644 --- a/backend/src/services/external-migration/external-migration-fns.ts +++ b/backend/src/services/external-migration/external-migration-fns.ts @@ -31,7 +31,7 @@ export type TImportDataIntoInfisicalDTO = { projectEnvDAL: Pick; kmsService: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; secretTagDAL: Pick; secretVersionTagDAL: Pick; diff --git a/backend/src/services/external-migration/external-migration-queue.ts b/backend/src/services/external-migration/external-migration-queue.ts index 94ca65fac..8aa46b94c 100644 --- a/backend/src/services/external-migration/external-migration-queue.ts +++ b/backend/src/services/external-migration/external-migration-queue.ts @@ -27,7 +27,7 @@ export type TExternalMigrationQueueFactoryDep = { projectEnvDAL: Pick; kmsService: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; secretTagDAL: Pick; secretVersionTagDAL: 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/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index d21ad3b9c..edc2efbe8 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -6,6 +6,7 @@ import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretImportDALFactory } from "./secret-import-dal"; +import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns"; type TSecretImportSecrets = { secretPath: string; @@ -39,6 +40,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 +152,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 +172,11 @@ 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 })[]; + }[] = [{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }]; const processedImports: TSecretImportSecretsV2[] = []; @@ -229,7 +235,8 @@ 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, 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 +274,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 3ce5b0115..6c412edc0 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -94,7 +94,7 @@ export const secretImportServiceFactory = ({ // check if user has permission to import from target path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment: data.environment, secretPath: data.path @@ -406,7 +406,7 @@ export const secretImportServiceFactory = ({ // check if user has permission to import from target path ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment: secretImportDoc.importEnv.slug, secretPath: secretImportDoc.importPath @@ -646,6 +646,7 @@ export const secretImportServiceFactory = ({ const importedSecrets = await fnSecretsV2FromImports({ secretImports, folderDAL, + viewSecretValue: true, secretDAL: secretV2BridgeDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), 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-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 4aea143d0..6eb95e9b9 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 @@ -148,7 +148,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 ({ @@ -286,7 +295,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 ({ @@ -627,7 +644,7 @@ export const reshapeBridgeSecret = ( }[]; secretMetadata?: ResourceMetadataDTO; }, - secretValueHidden?: boolean + secretValueHidden: boolean ) => ({ secretKey: secret.key, secretPath, 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 321015d19..c24b97ff4 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 @@ -323,11 +323,17 @@ export const secretV2BridgeServiceFactory = ({ }); } - return reshapeBridgeSecret(projectId, environment, secretPath, { - ...secret, - value: inputSecret.secretValue, - comment: inputSecret.secretComment || "" - }); + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...secret, + value: inputSecret.secretValue, + comment: inputSecret.secretComment || "" + }, + false + ); }; const updateSecret = async ({ @@ -407,9 +413,9 @@ 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(",")}` }); // now check with new ids ForbiddenError.from(permission).throwUnlessCan( @@ -418,7 +424,7 @@ export const secretV2BridgeServiceFactory = ({ environment, secretPath, secretName: inputSecret.secretName, - secretTags: tags?.map((el) => el.slug) + secretTags: newTags?.map((el) => el.slug) }) ); @@ -435,7 +441,7 @@ export const secretV2BridgeServiceFactory = ({ environment, secretPath, secretName: inputSecret.newSecretName, - secretTags: tags?.map((el) => el.slug) + secretTags: newTags?.map((el) => el.slug) }) ); } @@ -513,13 +519,14 @@ export const secretV2BridgeServiceFactory = ({ }); } + const tagsToCheck = newTags?.length ? newTags : secret.tags; const secretValueHidden = !permission.can( ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath, - secretName: inputSecret.secretName, // ! Note(Daniel): We are checking for the EXISTING secret name, not the new secret name. - secretTags: tags?.map((el) => el.slug) // ! Note(Daniel): Same here + secretName: inputSecret.secretName, + secretTags: tagsToCheck.length ? tagsToCheck.map((el) => el.slug) : undefined }) ); @@ -738,7 +745,13 @@ export const secretV2BridgeServiceFactory = ({ }; const getSecretsByFolderMappings = async ( - { projectId, userId, filters, folderMappings, filterByAction }: TGetSecretsRawByFolderMappingsDTO, + { + projectId, + userId, + filters, + folderMappings, + filterByAction = ProjectPermissionSecretActions.ReadValue + }: TGetSecretsRawByFolderMappingsDTO, projectPermission: Awaited>["permission"] ) => { const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId); @@ -755,13 +768,10 @@ export const secretV2BridgeServiceFactory = ({ projectId }); - // eslint-disable-next-line no-param-reassign - if (!filterByAction) filterByAction = ProjectPermissionSecretActions.ReadValue; - const decryptedSecrets = secrets .filter((el) => projectPermission.can( - filterByAction as ProjectPermissionSecretActions, // ? Typescript assumes that filterByAction may be undefined, which is not true, so we are casting it to ProjectPermissionSecretActions + filterByAction, subject(ProjectPermissionSub.Secrets, { environment: groupedFolderMappings[el.folderId][0].environment, secretPath: groupedFolderMappings[el.folderId][0].path, @@ -870,6 +880,7 @@ export const secretV2BridgeServiceFactory = ({ includeImports, recursive, expandSecretReferences: shouldExpandSecretReferences, + throwOnMissingReadValuePermission = true, ...params }: TGetSecretsDTO) => { const { permission } = await permissionService.getProjectPermission({ @@ -927,19 +938,7 @@ export const secretV2BridgeServiceFactory = ({ const decryptedSecrets = secrets .filter((el) => { - if (viewSecretValue) { - 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) - }) - ); - } - - return permission.can( + const canDescribeSecret = permission.can( ProjectPermissionSecretActions.DescribeSecret, subject(ProjectPermissionSub.Secrets, { environment, @@ -948,6 +947,43 @@ export const secretV2BridgeServiceFactory = ({ secretTags: el.tags.map((i) => i.slug) }) ); + + 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) + }) + ); + } + } + + return canDescribeSecret; }) .map((secret) => { const isPersonalSecret = secret.userId === actorId && secret.type === SecretType.Personal; @@ -1027,6 +1063,7 @@ 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, @@ -1174,6 +1211,7 @@ export const secretV2BridgeServiceFactory = ({ const secretImports = await secretImportDAL.find({ folderId, isReplication: false }); const importedSecrets = await fnSecretsV2FromImports({ secretImports, + viewSecretValue, secretDAL, folderDAL, secretImportDAL, @@ -1207,11 +1245,17 @@ export const secretV2BridgeServiceFactory = ({ 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 || "" - }); + return reshapeBridgeSecret( + projectId, + importedSecrets[i].environment, + importedSecrets[i].secretPath, + { + ...importedSecret, + value: importedSecret.secretValue || "", + comment: importedSecret.secretComment || "" + }, + importedSecret.secretValueHidden + ); } } } @@ -1365,8 +1409,8 @@ export const secretV2BridgeServiceFactory = ({ const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); - const newSecrets = await secretDAL.transaction(async (tx) => { - const createdSecrets = await fnSecretBulkInsert({ + const newSecrets = await secretDAL.transaction(async (tx) => + fnSecretBulkInsert({ inputSecrets: inputSecrets.map((el) => { const references = secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences; @@ -1395,19 +1439,8 @@ export const secretV2BridgeServiceFactory = ({ secretTagDAL, secretVersionTagDAL, tx - }); - - const secs = await secretDAL.find( - { - $in: { - [`${TableName.SecretV2}.id` as "id"]: createdSecrets.map((el) => el.id) - } - }, - { tx } - ); - - return secs; - }); + }) + ); await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ @@ -1656,7 +1689,7 @@ export const secretV2BridgeServiceFactory = ({ }); await $validateSecretReferences(projectId, permission, secretReferences, tx); - const bulkUpdatedSecretsRes = await fnSecretBulkUpdate({ + const bulkUpdatedSecrets = await fnSecretBulkUpdate({ folderId, orgId: actorOrgId, tx, @@ -1694,20 +1727,9 @@ export const secretV2BridgeServiceFactory = ({ resourceMetadataDAL }); - const bulkUpdatedSecrets = await secretDAL.find( - { - $in: { - [`${TableName.SecretV2}.id` as "id"]: bulkUpdatedSecretsRes.map((el) => el.id) - } - }, - { - tx - } - ); - updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); if (updateMode === SecretUpdateMode.Upsert) { - const bulkInsertedSecretsRes = await fnSecretBulkInsert({ + const bulkInsertedSecrets = await fnSecretBulkInsert({ inputSecrets: secretsToCreate.map((el) => { const references = secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences; @@ -1738,15 +1760,6 @@ export const secretV2BridgeServiceFactory = ({ tx }); - const bulkInsertedSecrets = await secretDAL.find( - { - $in: { - [`${TableName.SecretV2}.id` as "id"]: bulkInsertedSecretsRes.map((el) => el.id) - } - }, - { tx } - ); - updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); } } @@ -1957,11 +1970,17 @@ export const secretV2BridgeServiceFactory = ({ const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); return secretVersions.map((el) => - reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }) + reshapeBridgeSecret( + folder.projectId, + folder.environment.envSlug, + "/", + { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" + }, + false + ) ); }; 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 f3f3ebf0f..9cfe49b83 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 @@ -38,6 +38,7 @@ export type TGetSecretsDTO = { recursive?: boolean; tagSlugs?: string[]; viewSecretValue: boolean; + throwOnMissingReadValuePermission?: boolean; metadataFilter?: { key?: string; value?: string; @@ -50,6 +51,11 @@ export type TGetSecretsDTO = { keys?: string[]; } & TProjectPermission; +export type TGetSecretsMissingReadValuePermissionDTO = Omit< + TGetSecretsDTO, + "viewSecretValue" | "recursive" | "expandSecretReferences" +>; + export type TGetASecretDTO = { secretName: string; path: string; @@ -167,7 +173,7 @@ export type TFnSecretBulkInsert = { } >; resourceMetadataDAL: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; secretTagDAL: Pick; secretVersionTagDAL: Pick; @@ -191,7 +197,7 @@ export type TFnSecretBulkUpdate = { data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO }; }[]; resourceMetadataDAL: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; secretTagDAL: Pick; secretVersionTagDAL: Pick; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 00b0e7da8..65e3eaf4f 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -402,7 +402,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 9927a14c0..5c0806041 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -667,7 +667,15 @@ export const secretServiceFactory = ({ environment, secretPath: groupedPaths[secret.folderId][0].path })), - imports: importedSecrets + imports: importedSecrets.map((el) => { + return { + ...el, + secrets: el.secrets.map((secret) => ({ + ...secret, + secretValueHidden: false + })) + }; + }) }; } @@ -974,7 +982,7 @@ export const secretServiceFactory = ({ }); const secretValueHidden = !permission.can( - ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.ReadValue, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -1299,6 +1307,7 @@ export const secretServiceFactory = ({ expandSecretReferences, recursive, tagSlugs = [], + throwOnMissingReadValuePermission = true, ...paramsV2 }: TGetSecretsRawDTO) => { const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); @@ -1310,6 +1319,7 @@ export const secretServiceFactory = ({ actor, actorOrgId, viewSecretValue, + throwOnMissingReadValuePermission, environment, path, recursive, @@ -1318,6 +1328,7 @@ export const secretServiceFactory = ({ tagSlugs, ...paramsV2 }); + return { secrets, imports }; } @@ -1370,6 +1381,7 @@ export const secretServiceFactory = ({ const importedEntries = decryptedImportSecrets.reduce( ( accum: { + secretValueHidden: boolean; secretKey: string; secretPath: string; workspace: string; @@ -1413,6 +1425,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, @@ -1429,6 +1442,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, diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 06de34663..8ac9f58c0 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -181,6 +181,7 @@ export type TGetSecretsRawDTO = { path: string; environment: string; viewSecretValue: boolean; + throwOnMissingReadValuePermission?: boolean; includeImports?: boolean; recursive?: boolean; tagSlugs?: string[]; @@ -411,7 +412,7 @@ export type TCreateManySecretsRawFnFactory = { kmsService: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" + "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; @@ -448,7 +449,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/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx index b244aa5f2..6fbe96238 100644 --- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx +++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx @@ -2,7 +2,6 @@ import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useSt import { faCircle, faFolder, faKey } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as Popover from "@radix-ui/react-popover"; -import { twMerge } from "tailwind-merge"; import { useWorkspace } from "@app/context"; import { useDebounce, useToggle } from "@app/hooks"; @@ -283,7 +282,7 @@ export const InfisicalSecretInput = forwardRef( setIsFocused.off(); }} onChange={(e) => onChange?.(e.target.value)} - containerClassName={twMerge(containerClassName)} + containerClassName={containerClassName} /> {secretValueHidden ? ( -
+
********