From 8f8e8d3f8825f25ec7f6a219795a8575393bb4ba Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Wed, 17 Sep 2025 13:36:46 -0700 Subject: [PATCH 01/10] wip --- .../ee/services/audit-log/audit-log-types.ts | 39 +- .../src/server/routes/v1/dashboard-router.ts | 409 +++++++++++++++--- .../secret-v2-bridge-service.ts | 4 +- .../secret-v2-bridge-types.ts | 1 + backend/src/services/secret/secret-service.ts | 6 +- backend/src/services/secret/secret-types.ts | 1 + .../InfisicalSecretInput.tsx | 7 +- .../src/hooks/api/auditLogs/constants.tsx | 10 +- frontend/src/hooks/api/auditLogs/enums.tsx | 10 +- frontend/src/hooks/api/dashboard/queries.tsx | 76 +++- frontend/src/hooks/api/dashboard/types.ts | 19 +- .../src/hooks/api/secretImports/queries.tsx | 11 +- frontend/src/hooks/api/secretImports/types.ts | 2 +- frontend/src/hooks/api/secrets/queries.tsx | 46 +- frontend/src/hooks/api/secrets/types.ts | 11 + .../OverviewPage/OverviewPage.tsx | 4 +- .../SecretOverviewRotationSecretRow.tsx | 77 ++++ .../SecretOverviewSecretRotationRow.tsx | 57 +-- .../SecretOverviewTableRow/SecretEditRow.tsx | 107 ++++- .../SecretOverviewTableRow.tsx | 17 +- .../components/QuickSearchSecretItem.tsx | 41 +- .../SecretDashboardPage.tsx | 19 +- .../EnvironmentTabs/EnvironmentTabs.tsx | 7 +- .../SecretImportListView/SecretImportItem.tsx | 26 +- .../SecretImportListView.tsx | 12 +- .../SecretImportSecretRow.tsx | 71 +++ .../SecretListView/SecretDetailSidebar.tsx | 393 +++++++---------- .../components/SecretListView/SecretItem.tsx | 161 +++++-- .../SecretListView/SecretListView.tsx | 28 +- .../SecretListView/SecretVersionItem.tsx | 286 ++++++++++++ .../SecretRotationItem.tsx | 48 +- .../SecretRotationSecretRow.tsx | 76 ++++ 32 files changed, 1565 insertions(+), 517 deletions(-) create mode 100644 frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewSecretRotationRow/SecretOverviewRotationSecretRow.tsx create mode 100644 frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportSecretRow.tsx create mode 100644 frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretVersionItem.tsx create mode 100644 frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationSecretRow.tsx diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 07773885a..4d539fe4b 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -480,7 +480,11 @@ export enum EventType { CREATE_SECRET_REMINDER = "create-secret-reminder", GET_SECRET_REMINDER = "get-secret-reminder", - DELETE_SECRET_REMINDER = "delete-secret-reminder" + DELETE_SECRET_REMINDER = "delete-secret-reminder", + + DASHBOARD_LIST_SECRETS = "dashboard-list-secrets", + DASHBOARD_GET_SECRET_VALUE = "dashboard-get-secret-value", + DASHBOARD_GET_SECRET_VERSION_VALUE = "dashboard-get-secret-version-value" } export const filterableSecretEvents: EventType[] = [ @@ -3502,6 +3506,34 @@ interface ProjectDeleteEvent { }; } +interface DashboardListSecretsEvent { + type: EventType.DASHBOARD_LIST_SECRETS; + metadata: { + environment: string; + secretPath: string; + numberOfSecrets: number; + secretIds: string[]; + }; +} + +interface DashboardGetSecretValueEvent { + type: EventType.DASHBOARD_GET_SECRET_VALUE; + metadata: { + secretId: string; + secretKey: string; + environment: string; + secretPath: string; + }; +} + +interface DashboardGetSecretVersionValueEvent { + type: EventType.DASHBOARD_GET_SECRET_VERSION_VALUE; + metadata: { + secretId: string; + version: string; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -3818,4 +3850,7 @@ export type Event = | ProjectDeleteEvent | SecretReminderCreateEvent | SecretReminderGetEvent - | SecretReminderDeleteEvent; + | SecretReminderDeleteEvent + | DashboardListSecretsEvent + | DashboardGetSecretValueEvent + | DashboardGetSecretVersionValueEvent; diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index c394ee8e9..4e9d160bf 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -1,16 +1,16 @@ import { ForbiddenError } from "@casl/ability"; import { z } from "zod"; -import { SecretFoldersSchema, SecretImportsSchema, UsersSchema } from "@app/db/schemas"; +import { SecretFoldersSchema, SecretImportsSchema, SecretType, UsersSchema } from "@app/db/schemas"; import { RemindersSchema } from "@app/db/schemas/reminders"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; import { DASHBOARD } from "@app/lib/api-docs"; -import { BadRequestError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; import { OrderByDirection } from "@app/lib/types"; -import { secretsLimit } from "@app/server/config/rateLimiter"; +import { readLimit, 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"; @@ -111,6 +111,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { SecretRotationV2Schema, z.object({ secrets: secretRawSchema + .omit({ secretValue: true }) .extend({ secretValueHidden: z.boolean(), secretPath: z.string().optional(), @@ -124,7 +125,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { .array() .optional(), secrets: secretRawSchema + .omit({ secretValue: true }) .extend({ + isEmpty: z.boolean(), secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), @@ -219,7 +222,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { let imports: Awaited> | undefined; let folders: Awaited> | undefined; - let secrets: Awaited> | undefined; + let secrets: + | (Awaited>[number] & { isEmpty: boolean })[] + | undefined; let dynamicSecrets: | Awaited> | undefined; @@ -426,43 +431,51 @@ 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, - environments, - actorAuthMethod: req.permission.authMethod, - projectId, - path: secretPath, - orderBy, - orderDirection, - search, - limit: remainingLimit, - offset: adjustedOffset, - isInternal: true - }); + secrets = ( + await server.services.secret.getSecretsRawMultiEnv({ + viewSecretValue: true, + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + environments, + actorAuthMethod: req.permission.authMethod, + projectId, + path: secretPath, + orderBy, + orderDirection, + search, + limit: remainingLimit, + offset: adjustedOffset, + isInternal: true + }) + ).map((secret) => ({ ...secret, isEmpty: !secret.secretValue })); } } if (secrets?.length || secretRotations?.length) { for await (const environment of environments) { - const secretCountFromEnv = - (secrets?.filter((secret) => secret.environment === environment).length ?? 0) + - (secretRotations - ?.filter((rotation) => rotation.environment.slug === environment) - .flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))).length ?? 0); + const secretIds = [ + ...new Set( + [ + ...(secrets?.filter((secret) => secret.environment === environment) ?? []), + ...(secretRotations + ?.filter((rotation) => rotation.environment.slug === environment) + .flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))) ?? []) + ].map((secret) => secret.id) + ) + ]; - if (secretCountFromEnv) { + if (secretIds) { await server.services.auditLog.createAuditLog({ projectId, ...req.auditLogInfo, event: { - type: EventType.GET_SECRETS, + type: EventType.DASHBOARD_LIST_SECRETS, metadata: { environment, secretPath, - numberOfSecrets: secretCountFromEnv + numberOfSecrets: secretIds.length, + secretIds } } }); @@ -473,7 +486,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { distinctId: getTelemetryDistinctId(req), organizationId: req.permission.orgId, properties: { - numberOfSecrets: secretCountFromEnv, + numberOfSecrets: secretIds.length, projectId, environment, secretPath, @@ -584,7 +597,6 @@ 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), @@ -606,7 +618,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { SecretRotationV2Schema, z.object({ secrets: secretRawSchema + .omit({ secretValue: true }) .extend({ + isEmpty: z.boolean(), secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), @@ -619,7 +633,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { .array() .optional(), secrets: secretRawSchema + .omit({ secretValue: true }) .extend({ + isEmpty: z.boolean(), secretReminderRecipients: z .object({ user: UsersSchema.pick({ id: true, email: true, username: true }), @@ -715,12 +731,21 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { let folders: Awaited> | undefined; let secrets: | (Awaited>["secrets"][number] & { + isEmpty: boolean; reminder: Awaited>[string] | null; })[] | undefined; let dynamicSecrets: Awaited> | undefined; let secretRotations: - | Awaited> + | (Awaited>[number] & { + secrets: (NonNullable< + Awaited< + ReturnType + >[number]["secrets"][number] & { + isEmpty: boolean; + } + > | null)[]; + })[] | undefined; let totalImportCount: number | undefined; @@ -822,19 +847,31 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { ); if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) { - secretRotations = await server.services.secretRotationV2.getDashboardSecretRotations( - { - projectId, - search, - orderBy, - orderDirection, - environments: [environment], - secretPath, - limit: remainingLimit, - offset: adjustedOffset - }, - req.permission - ); + secretRotations = ( + await server.services.secretRotationV2.getDashboardSecretRotations( + { + projectId, + search, + orderBy, + orderDirection, + environments: [environment], + secretPath, + limit: remainingLimit, + offset: adjustedOffset + }, + req.permission + ) + ).map((rotation) => ({ + ...rotation, + secrets: rotation.secrets.map((secret) => + secret + ? { + ...secret, + isEmpty: !secret.secretValue + } + : secret + ) + })); await server.services.auditLog.createAuditLog({ projectId, @@ -919,7 +956,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { await server.services.secret.getSecretsRaw({ actorId: req.permission.id, actor: req.permission.type, - viewSecretValue: req.query.viewSecretValue, + viewSecretValue: true, throwOnMissingReadValuePermission: false, actorOrgId: req.permission.orgId, environment, @@ -943,6 +980,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { secrets = rawSecrets.map((secret) => ({ ...secret, + isEmpty: !secret.secretValue, reminder: reminders[secret.id] ?? null })); } @@ -977,19 +1015,25 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { })); if (secrets?.length || secretRotations?.length) { - const secretCount = - (secrets?.length ?? 0) + - (secretRotations?.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))).length ?? 0); + const secretIds = [ + ...new Set( + [ + ...(secrets ?? []), + ...(secretRotations?.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))) ?? []) + ].map((secret) => secret.id) + ) + ]; await server.services.auditLog.createAuditLog({ projectId, ...req.auditLogInfo, event: { - type: EventType.GET_SECRETS, + type: EventType.DASHBOARD_LIST_SECRETS, metadata: { environment, secretPath, - numberOfSecrets: secretCount + numberOfSecrets: secretIds.length, + secretIds } } }); @@ -1000,7 +1044,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { distinctId: getTelemetryDistinctId(req), organizationId: req.permission.orgId, properties: { - numberOfSecrets: secretCount, + numberOfSecrets: secretIds.length, projectId, environment, secretPath, @@ -1060,6 +1104,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { .array() .optional(), secrets: secretRawSchema + .omit({ secretValue: true }) .extend({ secretValueHidden: z.boolean(), secretPath: z.string().optional(), @@ -1145,18 +1190,20 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { ); for await (const environment of environments) { - const secretCountForEnv = secrets.filter((secret) => secret.environment === environment).length; + const envSecrets = secrets.filter((secret) => secret.environment === environment); + const secretCountForEnv = envSecrets.length; if (secretCountForEnv) { await server.services.auditLog.createAuditLog({ projectId, ...req.auditLogInfo, event: { - type: EventType.GET_SECRETS, + type: EventType.DASHBOARD_LIST_SECRETS, metadata: { environment, secretPath, - numberOfSecrets: secretCountForEnv + numberOfSecrets: secretCountForEnv, + secretIds: envSecrets.map((secret) => secret.id) } } }); @@ -1259,6 +1306,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ + // TODO(scott): omit secretValue here, but requires refactor of uploading env/copy from board secrets: secretRawSchema .extend({ secretPath: z.string().optional(), @@ -1310,6 +1358,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ + // TODO(scott): omit secretValue here, but requires refactor of uploading env/copy from board secrets: secretRawSchema .extend({ secretValueHidden: z.boolean(), @@ -1345,11 +1394,12 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { projectId, ...req.auditLogInfo, event: { - type: EventType.GET_SECRETS, + type: EventType.DASHBOARD_LIST_SECRETS, metadata: { environment, secretPath, - numberOfSecrets: secrets.length + numberOfSecrets: secrets.length, + secretIds: secrets.map((secret) => secret.id) } } }); @@ -1373,4 +1423,251 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { return { secrets }; } }); + + server.route({ + method: "GET", + url: "/secret-value", + config: { + rateLimit: secretsLimit + }, + schema: { + security: [ + { + bearerAuth: [] + } + ], + querystring: z.object({ + projectId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().default("/").transform(removeTrailingSlash), + secretKey: z.string().trim(), + isOverride: z + .enum(["true", "false"]) + .transform((value) => value === "true") + .optional() + }), + response: { + 200: z.object({ + valueOverride: z.string().optional(), + value: z.string().optional() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { secretPath, projectId, environment, secretKey, isOverride } = req.query; + + const { secrets } = await server.services.secret.getSecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + viewSecretValue: true, + throwOnMissingReadValuePermission: false, + actorOrgId: req.permission.orgId, + environment, + actorAuthMethod: req.permission.authMethod, + projectId, + path: secretPath, + search: secretKey, + includeTagsInSearch: true, + includeMetadataInSearch: true + }); + + if (isOverride) { + const personalSecret = secrets.find((secret) => secret.type === SecretType.Personal); + + if (!personalSecret) + throw new BadRequestError({ + message: `Could not find personal secret with key "${secretKey}" at secret path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"` + }); + + if (personalSecret) + return { + valueOverride: personalSecret.secretValue + }; + } + + const sharedSecret = secrets.find((secret) => secret.type === SecretType.Shared); + + if (!sharedSecret) + throw new BadRequestError({ + message: `Could not find secret with key "${secretKey}" at secret path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"` + }); + + // only audit if not personal + await server.services.auditLog.createAuditLog({ + projectId, + ...req.auditLogInfo, + event: { + type: EventType.DASHBOARD_GET_SECRET_VALUE, + metadata: { + environment: req.query.environment, + secretPath: req.query.secretPath, + secretKey, + secretId: sharedSecret.id + } + } + }); + + return { value: sharedSecret.secretValue }; + } + }); + + server.route({ + url: "/secret-imports", + method: "GET", + config: { + rateLimit: secretsLimit + }, + schema: { + querystring: z.object({ + projectId: z.string().trim(), + environment: z.string().trim(), + path: z.string().trim().default("/").transform(removeTrailingSlash) + }), + response: { + 200: z.object({ + secrets: z + .object({ + secretPath: z.string(), + environment: z.string(), + environmentInfo: z.object({ + id: z.string(), + name: z.string(), + slug: z.string() + }), + folderId: z.string().optional(), + secrets: secretRawSchema.omit({ secretValue: true }).extend({ isEmpty: z.boolean() }).array() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const importedSecrets = await server.services.secretImport.getRawSecretsFromImports({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.query.projectId, + ...req.auditLogInfo, + event: { + type: EventType.DASHBOARD_LIST_SECRETS, + metadata: { + environment: req.query.environment, + secretPath: req.query.path, + numberOfSecrets: importedSecrets.length, + secretIds: importedSecrets.map((secret) => secret.id) + } + } + }); + + return { + secrets: importedSecrets.map((importData) => ({ + ...importData, + secrets: importData.secrets.map((secret) => ({ + ...secret, + isEmpty: !secret.secretValue + })) + })) + }; + } + }); + + server.route({ + method: "GET", + url: "/secret-versions/:secretId", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + secretId: z.string() + }), + querystring: z.object({ + offset: z.coerce.number(), + limit: z.coerce.number() + }), + response: { + 200: z.object({ + secretVersions: secretRawSchema + .omit({ secretValue: true }) + .extend({ + secretValueHidden: z.boolean() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const secretVersions = await server.services.secret.getSecretVersions({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + limit: req.query.limit, + offset: req.query.offset, + secretId: req.params.secretId + }); + + return { secretVersions }; + } + }); + + server.route({ + method: "GET", + url: "/secret-versions/:secretId/value/:version", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + secretId: z.string(), + version: z.string() + }), + + response: { + 200: z.object({ + value: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { version, secretId } = req.params; + + const [secretVersion] = await server.services.secret.getSecretVersions({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + secretId, + secretVersions: [version] + }); + + if (!secretVersion) + throw new NotFoundError({ + message: `Could not find secret version "${version}" for secret with ID "${secretId}` + }); + + await server.services.auditLog.createAuditLog({ + projectId: secretVersion.workspace, + ...req.auditLogInfo, + event: { + type: EventType.DASHBOARD_GET_SECRET_VERSION_VALUE, + metadata: { + secretId, + version + } + } + }); + + return { value: secretVersion.secretValue }; + } + }); }; 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 9db535fbe..2ed543c81 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 @@ -2333,7 +2333,8 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, limit = 20, offset = 0, - secretId + secretId, + secretVersions: secretVersionsFilter }: TGetSecretVersionsDTO) => { const secret = await secretDAL.findById(secretId); @@ -2370,6 +2371,7 @@ export const secretV2BridgeServiceFactory = ({ const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors({ secretId, projectId: folder.projectId, + secretVersions: secretVersionsFilter, findOpt: { offset, limit, 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 fc7d468ff..5e2ffc1a0 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 @@ -159,6 +159,7 @@ export type TGetSecretVersionsDTO = Omit & { limit?: number; offset?: number; secretId: string; + secretVersions?: string[]; }; export type TSecretReference = { environment: string; secretPath: string; secretKey: string }; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 8758a992e..3a5ccc667 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -2568,7 +2568,8 @@ export const secretServiceFactory = ({ actorAuthMethod, limit = 20, offset = 0, - secretId + secretId, + secretVersions: filterSecretVersions }: TGetSecretVersionsDTO) => { const secretVersionV2 = await secretV2BridgeService .getSecretVersions({ @@ -2578,7 +2579,8 @@ export const secretServiceFactory = ({ actorAuthMethod, limit, offset, - secretId + secretId, + secretVersions: filterSecretVersions }) .catch((err) => { if ((err as Error).message === "BadRequest: Failed to find secret") { diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 07f46bf1e..d8c778d7e 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -331,6 +331,7 @@ export type TGetSecretVersionsDTO = Omit & { limit?: number; offset?: number; secretId: string; + secretVersions?: string[]; }; export type TSecretReference = { environment: string; secretPath: string }; diff --git a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx index ec09a93eb..8c26dfc81 100644 --- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx +++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx @@ -279,11 +279,16 @@ export const InfisicalSecretInput = forwardRef( ref={handleRef} onKeyDown={handleKeyDown} value={value} - onFocus={() => setIsFocused.on()} + onFocus={(evt) => { + if (props.onFocus) props.onFocus(evt); + setIsFocused.on(); + }} onBlur={(evt) => { // should not on blur when its mouse down selecting a item from suggestion if (!(evt.relatedTarget?.getAttribute("aria-label") === "suggestion-item")) setIsFocused.off(); + + if (props.onBlur) props.onBlur(evt); }} onChange={(e) => onChange?.(e.target.value)} containerClassName={containerClassName} diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index 503264fb2..d770a682f 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -222,7 +222,15 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.UPDATE_ORG]: "Update Organization", [EventType.CREATE_PROJECT]: "Create Project", [EventType.UPDATE_PROJECT]: "Update Project", - [EventType.DELETE_PROJECT]: "Delete Project" + [EventType.DELETE_PROJECT]: "Delete Project", + + [EventType.CREATE_SECRET_REMINDER]: "Create Secret Reminder", + [EventType.GET_SECRET_REMINDER]: "Get Secret Reminder", + [EventType.DELETE_SECRET_REMINDER]: "Delete Secret Reminder", + + [EventType.DASHBOARD_LIST_SECRETS]: "Dashboard List Secrets", + [EventType.DASHBOARD_GET_SECRET_VALUE]: "Dashboard Get Secret Value", + [EventType.DASHBOARD_GET_SECRET_VERSION_VALUE]: "Dashboard Get Secret Version Value" }; export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = { diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index bfa983188..135319713 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -216,5 +216,13 @@ export enum EventType { CREATE_PROJECT = "create-project", UPDATE_PROJECT = "update-project", - DELETE_PROJECT = "delete-project" + DELETE_PROJECT = "delete-project", + + CREATE_SECRET_REMINDER = "create-secret-reminder", + GET_SECRET_REMINDER = "get-secret-reminder", + DELETE_SECRET_REMINDER = "delete-secret-reminder", + + DASHBOARD_LIST_SECRETS = "dashboard-list-secrets", + DASHBOARD_GET_SECRET_VALUE = "dashboard-get-secret-value", + DASHBOARD_GET_SECRET_VERSION_VALUE = "dashboard-get-secret-version-value" } diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx index fde3a5ee3..0d2a0b8be 100644 --- a/frontend/src/hooks/api/dashboard/queries.tsx +++ b/frontend/src/hooks/api/dashboard/queries.tsx @@ -1,5 +1,5 @@ import { useCallback } from "react"; -import { useQuery, UseQueryOptions } from "@tanstack/react-query"; +import { useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query"; import { AxiosError } from "axios"; import { apiRequest } from "@app/config/request"; @@ -10,13 +10,15 @@ import { DashboardProjectSecretsOverview, DashboardProjectSecretsOverviewResponse, DashboardSecretsOrderBy, + DashboardSecretValue, TDashboardProjectSecretsQuickSearch, TDashboardProjectSecretsQuickSearchResponse, TGetAccessibleSecretsDTO, TGetDashboardProjectSecretsByKeys, TGetDashboardProjectSecretsDetailsDTO, TGetDashboardProjectSecretsOverviewDTO, - TGetDashboardProjectSecretsQuickSearchDTO + TGetDashboardProjectSecretsQuickSearchDTO, + TGetSecretValueDTO } from "@app/hooks/api/dashboard/types"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries"; @@ -73,6 +75,15 @@ export const dashboardKeys = { ...dashboardKeys.all(), "accessible-secrets", { projectId, secretPath, environment, filterByAction } + ] as const, + getSecretValuesRoot: () => [...dashboardKeys.all(), "secrets-values"] as const, + getSecretValue: ({ environment, secretPath, secretKey, isOverride }: TGetSecretValueDTO) => + [ + ...dashboardKeys.getSecretValuesRoot(), + environment, + secretPath, + secretKey, + isOverride ] as const }; @@ -174,6 +185,8 @@ export const useGetProjectSecretsOverview = ( "queryKey" | "queryFn" > ) => { + const queryClient = useQueryClient(); + return useQuery({ ...options, // wait for all values to be available @@ -193,8 +206,8 @@ export const useGetProjectSecretsOverview = ( includeSecretRotations, environments }), - queryFn: () => - fetchProjectSecretsOverview({ + queryFn: async () => { + const resp = fetchProjectSecretsOverview({ secretPath, search, limit, @@ -208,7 +221,14 @@ export const useGetProjectSecretsOverview = ( includeDynamicSecrets, includeSecretRotations, environments - }), + }); + + queryClient.resetQueries({ + queryKey: dashboardKeys.getSecretValuesRoot() + }); + + return resp; + }, select: useCallback((data: Awaited>) => { const { secrets, secretRotations, ...select } = data; const uniqueSecrets = secrets ? unique(secrets, (i) => i.secretKey) : []; @@ -254,7 +274,6 @@ export const useGetProjectSecretsDetails = ( search = "", includeSecrets, includeFolders, - viewSecretValue, includeImports, includeDynamicSecrets, includeSecretRotations, @@ -270,6 +289,8 @@ export const useGetProjectSecretsDetails = ( "queryKey" | "queryFn" > ) => { + const queryClient = useQueryClient(); + return useQuery({ ...options, // wait for all values to be available @@ -286,7 +307,6 @@ export const useGetProjectSecretsDetails = ( limit, orderBy, orderDirection, - viewSecretValue, offset, projectId, environment, @@ -297,14 +317,13 @@ export const useGetProjectSecretsDetails = ( includeSecretRotations, tags }), - queryFn: () => - fetchProjectSecretsDetails({ + queryFn: async () => { + const resp = await fetchProjectSecretsDetails({ secretPath, search, limit, orderBy, orderDirection, - viewSecretValue, offset, projectId, environment, @@ -314,7 +333,14 @@ export const useGetProjectSecretsDetails = ( includeDynamicSecrets, includeSecretRotations, tags - }), + }); + + queryClient.resetQueries({ + queryKey: dashboardKeys.getSecretValuesRoot() + }); + + return resp; + }, select: useCallback( (data: Awaited>) => ({ ...data, @@ -471,3 +497,31 @@ export const useGetAccessibleSecrets = ({ fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction, recursive }) }); }; + +export const fetchSecretValue = async (params: TGetSecretValueDTO) => { + const { data } = await apiRequest.get("/api/v1/dashboard/secret-value", { + params + }); + + return data; +}; + +export const useGetSecretValue = ( + params: TGetSecretValueDTO, + options?: Omit< + UseQueryOptions< + DashboardSecretValue, + unknown, + DashboardSecretValue, + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: dashboardKeys.getSecretValue(params), + queryFn: async () => fetchSecretValue(params), + staleTime: 1000 * 60, + ...options + }); +}; diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index fbd5107cd..5a7a519b7 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -112,7 +112,6 @@ export type TGetDashboardProjectSecretsDetailsDTO = Omit< TGetDashboardProjectSecretsOverviewDTO, "environments" > & { - viewSecretValue: boolean; environment: string; includeImports?: boolean; tags: Record; @@ -156,3 +155,21 @@ export type TGetAccessibleSecretsDTO = { | ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue; }; + +export type TGetSecretValueDTO = { + projectId: string; + secretKey: string; + environment: string; + secretPath: string; + isOverride?: boolean; +}; + +export type DashboardSecretValue = + | { + value: string; + valueOverride: undefined; + } + | { + value: undefined; + valueOverride: string; + }; diff --git a/frontend/src/hooks/api/secretImports/queries.tsx b/frontend/src/hooks/api/secretImports/queries.tsx index 71716cb7c..5d80c3649 100644 --- a/frontend/src/hooks/api/secretImports/queries.tsx +++ b/frontend/src/hooks/api/secretImports/queries.tsx @@ -67,7 +67,7 @@ export const useGetSecretImports = ({ const fetchImportedSecrets = async (projectId: string, environment: string, directory?: string) => { const { data } = await apiRequest.get<{ secrets: TImportedSecrets[] }>( - "/api/v2/secret-imports/secrets", + "/api/v1/dashboard/secret-imports", { params: { projectId, @@ -132,13 +132,13 @@ export const useGetImportedSecretsSingleEnv = ({ id: encSecret.id, env: encSecret.environment, key: encSecret.secretKey, - value: encSecret.secretValue, secretValueHidden: encSecret.secretValueHidden, tags: encSecret.tags, comment: encSecret.secretComment, createdAt: encSecret.createdAt, updatedAt: encSecret.updatedAt, - version: encSecret.version + version: encSecret.version, + isEmpty: encSecret.isEmpty }; }) })); @@ -172,7 +172,6 @@ export const useGetImportedSecretsAllEnvs = ({ id: encSecret.id, env: encSecret.environment, key: encSecret.secretKey, - value: encSecret.secretValue, secretValueHidden: encSecret.secretValueHidden, tags: encSecret.tags, comment: encSecret.secretComment, @@ -233,7 +232,9 @@ export const useGetImportedSecretsAllEnvs = ({ return { secret: secret?.secrets.find((s) => s.key === secretName), - environmentInfo: secret?.environmentInfo + environmentInfo: secret?.environmentInfo, + secretPath: secret?.secretPath, + environment: secret?.environment }; } return undefined; diff --git a/frontend/src/hooks/api/secretImports/types.ts b/frontend/src/hooks/api/secretImports/types.ts index 23690b33d..d17641398 100644 --- a/frontend/src/hooks/api/secretImports/types.ts +++ b/frontend/src/hooks/api/secretImports/types.ts @@ -28,7 +28,7 @@ export type TImportedSecrets = { environmentInfo: ProjectEnv; secretPath: string; folderId: string; - secrets: SecretV3Raw[]; + secrets: Omit[]; }; export type TGetSecretImports = { diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index df3a78c35..b12cca38b 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -21,7 +21,9 @@ import { TGetProjectSecretsKey, TGetSecretAccessListDTO, TGetSecretReferenceTreeDTO, - TSecretReferenceTraceNode + TGetSecretVersionValue, + TSecretReferenceTraceNode, + TSecretVersionValue } from "./types"; export const secretKeys = { @@ -34,6 +36,8 @@ export const secretKeys = { }: TGetProjectSecretsKey) => [{ projectId, environment, secretPath, viewSecretValue }, "secrets"] as const, getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const, + getSecretVersionValue: (secretId: string, version: number) => + ["secret-versions", secretId, version] as const, getSecretAccessList: ({ projectId, environment, @@ -67,7 +71,10 @@ export const fetchProjectSecrets = async ({ }; export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => { - const personalSecrets: Record = {}; + const personalSecrets: Record< + string, + { id: string; value?: string; env: string; isEmpty?: boolean } + > = {}; const secrets: SecretV3RawSanitized[] = []; rawSecrets.forEach((el) => { const decryptedSecret: SecretV3RawSanitized = { @@ -89,14 +96,16 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => { secretMetadata: el.secretMetadata, isRotatedSecret: el.isRotatedSecret, rotationId: el.rotationId, - reminder: el.reminder + reminder: el.reminder, + isEmpty: el.isEmpty }; if (el.type === SecretType.Personal) { personalSecrets[decryptedSecret.key] = { id: el.id, value: el.secretValue, - env: el.environment + env: el.environment, + isEmpty: el.isEmpty }; } else { secrets.push(decryptedSecret); @@ -109,6 +118,7 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => { sec.idOverride = personalSecret.id; sec.valueOverride = personalSecret.value; sec.overrideAction = "modified"; + sec.isEmpty = personalSecret.isEmpty; } }); @@ -238,7 +248,7 @@ export const useGetProjectSecretsAllEnv = ({ const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => { const { data } = await apiRequest.get<{ secretVersions: SecretVersions[] }>( - `/api/v1/secret/${secretId}/secret-versions`, + `/api/v1/dashboard/secret-versions/${secretId}`, { params: { limit, @@ -259,6 +269,32 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) => }, []) }); +export const fetchSecretVersionValue = async (secretId: string, version: number) => { + const { data } = await apiRequest.get( + `/api/v1/dashboard/secret-versions/${secretId}/value/${version}`, + { + params: { + secretId, + version + } + } + ); + return data.value; +}; + +export const useGetSecretVersionValue = ( + dto: TGetSecretVersionValue, + options?: Omit< + UseQueryOptions>, + "queryKey" | "queryFn" + > +) => + useQuery({ + queryKey: secretKeys.getSecretVersionValue(dto.secretId, dto.version), + queryFn: () => fetchSecretVersionValue(dto.secretId, dto.version), + ...options + }); + export const useGetSecretAccessList = (dto: TGetSecretAccessListDTO) => useQuery({ enabled: Boolean(dto.secretKey), diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 327d3659d..68a3852c0 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -47,6 +47,7 @@ export type SecretV3RawSanitized = { isPending?: boolean; pendingAction?: PendingAction; reminder?: Reminder; + isEmpty?: boolean; }; export type SecretV3Raw = { @@ -73,6 +74,7 @@ export type SecretV3Raw = { rotationId?: string; secretReminderRecipients?: SecretReminderRecipient[]; reminder?: Reminder; + isEmpty?: boolean; }; export type SecretV3RawResponse = { @@ -137,6 +139,15 @@ export type GetSecretVersionsDTO = { offset: number; }; +export type TGetSecretVersionValue = { + secretId: string; + version: number; +}; + +export type TSecretVersionValue = { + value: string; +}; + export type TGetSecretAccessListDTO = { projectId: string; environment: string; diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index ab9b332b0..28233ca61 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -349,9 +349,7 @@ export const OverviewPage = () => { getSecretRotationStatusesByName } = useSecretRotationOverview(secretRotations); - const { secKeys, getEnvSecretKeyCount } = useSecretOverview( - secrets?.concat(secretImportsShaped) || [] - ); + const { secKeys, getEnvSecretKeyCount } = useSecretOverview(secrets || []); const getSecretByKey = useCallback( (env: string, key: string) => { diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewSecretRotationRow/SecretOverviewRotationSecretRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewSecretRotationRow/SecretOverviewRotationSecretRow.tsx new file mode 100644 index 000000000..e8df569b9 --- /dev/null +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewSecretRotationRow/SecretOverviewRotationSecretRow.tsx @@ -0,0 +1,77 @@ +import { twMerge } from "tailwind-merge"; + +import { SecretInput, Tooltip } from "@app/components/v2"; +import { Blur } from "@app/components/v2/Blur"; +import { useProject } from "@app/context"; +import { useToggle } from "@app/hooks"; +import { useGetSecretValue } from "@app/hooks/api/dashboard/queries"; +import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; +import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem"; + +interface SecretOverviewRotationSecretRowProps { + secret: SecretV3RawSanitized | null; + isSecretVisible: boolean; + environment: string; + secretPath: string; +} + +export const SecretOverviewRotationSecretRow = ({ + secret, + isSecretVisible, + environment, + secretPath +}: SecretOverviewRotationSecretRowProps) => { + const [isFieldFocused, setIsFieldFocused] = useToggle(); + + const { currentProject } = useProject(); + + const canFetchValue = Boolean(secret); + + const { data: secretValueData, isError } = useGetSecretValue( + { + secretKey: secret!.key, + environment, + secretPath, + projectId: currentProject.id + }, + { + enabled: canFetchValue && (isSecretVisible || isFieldFocused) + } + ); + + const secretValue = isError + ? "Error loading secret value..." + : (secretValueData?.valueOverride ?? secretValueData?.value ?? HIDDEN_SECRET_VALUE); + + return ( + + + + {secret?.key ?? "********"} + + + {/* eslint-disable-next-line no-nested-ternary */} + {!secret ? ( +
********
+ ) : secret.secretValueHidden ? ( + + ) : ( + setIsFieldFocused.on()} + onBlur={() => setIsFieldFocused.off()} + /> + )} + + +
+ ); +}; diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewSecretRotationRow/SecretOverviewSecretRotationRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewSecretRotationRow/SecretOverviewSecretRotationRow.tsx index 17f328fe2..46a5e7328 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewSecretRotationRow/SecretOverviewSecretRotationRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewSecretRotationRow/SecretOverviewSecretRotationRow.tsx @@ -16,8 +16,6 @@ import { twMerge } from "tailwind-merge"; import { ProjectPermissionCan } from "@app/components/permissions"; import { SecretRotationV2StatusBadge } from "@app/components/secret-rotations-v2/SecretRotationV2StatusBadge"; import { Badge, IconButton, TableContainer, Tag, Td, Tooltip, Tr } from "@app/components/v2"; -import { Blur } from "@app/components/v2/Blur"; -import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { ProjectPermissionSecretRotationActions, ProjectPermissionSub @@ -27,6 +25,8 @@ import { useToggle } from "@app/hooks"; import { SecretRotationStatus, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2"; import { getExpandedRowStyle } from "@app/pages/secret-manager/OverviewPage/components/utils"; +import { SecretOverviewRotationSecretRow } from "./SecretOverviewRotationSecretRow"; + type Props = { secretRotationName: string; environments: { name: string; slug: string }[]; @@ -256,52 +256,13 @@ export const SecretOverviewSecretRotationRow = ({ {secrets.map((secret, index) => { return ( - - - - - {secret?.key ?? "********"} - - - - {/* eslint-disable-next-line no-nested-ternary */} - {!secret ? ( -
********
- ) : secret.secretValueHidden ? ( - - ) : ( - {}} - /> - )} - - -
+ ); })} 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 4b07e6242..84b58fb7f 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -10,6 +10,7 @@ import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useQueryClient } from "@tanstack/react-query"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; @@ -27,12 +28,23 @@ import { Tooltip } from "@app/components/v2"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; -import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProject, + useProjectPermission +} from "@app/context"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { usePopUp, useToggle } from "@app/hooks"; -import { SecretType } from "@app/hooks/api/types"; +import { + dashboardKeys, + fetchSecretValue, + useGetSecretValue +} from "@app/hooks/api/dashboard/queries"; +import { ProjectEnv, SecretType, SecretV3RawSanitized } from "@app/hooks/api/types"; import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { CollapsibleSecretImports } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports"; +import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem"; type Props = { defaultValue?: string | null; @@ -56,6 +68,15 @@ type Props = { ) => Promise; onSecretDelete: (env: string, key: string, secretId?: string) => Promise; isRotatedSecret?: boolean; + isEmpty?: boolean; + importedSecret?: + | { + secretPath: string; + secret?: SecretV3RawSanitized; + environmentInfo?: ProjectEnv; + environment: string; + } + | undefined; importedBy?: { environment: { name: string; slug: string }; folders: { @@ -81,12 +102,49 @@ export const SecretEditRow = ({ isVisible, secretId, isRotatedSecret, - importedBy + importedBy, + importedSecret, + isEmpty }: Props) => { const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ "editSecret" ] as const); + const queryClient = useQueryClient(); + + const { currentProject } = useProject(); + + const [isFieldFocused, setIsFieldFocused] = useToggle(); + + const fetchSecretValueParams = importedSecret + ? { + environment: importedSecret.environment, + secretPath: importedSecret.secretPath, + secretKey: importedSecret.secret!.key, + projectId: currentProject.id + } + : { + environment, + secretPath, + secretKey: secretName, + projectId: currentProject.id, + isOverride + }; + + // scott: only fetch value if secret exists, has non-empty value and user has permission + const canFetchValue = Boolean(importedSecret ?? secretId) && !isEmpty && !secretValueHidden; + + const { + data: secretValueData, + isPending: isPendingSecretValueData, + isError: isErrorFetchingSecretValue + } = useGetSecretValue(fetchSecretValueParams, { + enabled: canFetchValue && (isVisible || isFieldFocused) + }); + + const isFetchingSecretValue = canFetchValue && isPendingSecretValueData; + const isSecretValueFetched = Boolean(secretValueData); + const { handleSubmit, control, @@ -95,7 +153,7 @@ export const SecretEditRow = ({ formState: { isDirty, isSubmitting } } = useForm({ values: { - value: defaultValue || null + value: (secretValueData?.valueOverride ?? secretValueData?.value) || null } }); @@ -113,6 +171,25 @@ export const SecretEditRow = ({ }; const handleCopySecretToClipboard = async () => { + if (!isSecretValueFetched) { + try { + const data = await fetchSecretValue(fetchSecretValueParams); + + queryClient.setQueryData(dashboardKeys.getSecretValue(fetchSecretValueParams), data); + + await window.navigator.clipboard.writeText(data.valueOverride ?? data.value); + createNotification({ type: "success", text: "Copied secret to clipboard" }); + return; + } catch (e) { + console.error(e); + createNotification({ + type: "error", + text: "Failed to fetch secret value." + }); + return; + } + } + const { value } = getValues(); if (value) { try { @@ -221,14 +298,25 @@ export const SecretEditRow = ({ )}
( setIsFieldFocused.on()} + onBlur={() => { + field.onBlur(); + setIsFieldFocused.off(); + }} /> )} /> 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 0260135f8..cb2307880 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx @@ -50,7 +50,14 @@ type Props = { getImportedSecretByKey: ( env: string, secretName: string - ) => { secret?: SecretV3RawSanitized; environmentInfo?: ProjectEnv } | undefined; + ) => + | { + secret?: SecretV3RawSanitized; + secretPath: string; + environment: string; + environmentInfo?: ProjectEnv; + } + | undefined; scrollOffset: number; importedBy?: { environment: { name: string; slug: string }; @@ -140,7 +147,7 @@ export const SecretOverviewTableRow = ({ const isSecretImported = isImportedSecretPresentInEnv(slug, secretKey); const isSecretPresent = Boolean(secret); - const isSecretEmpty = secret?.value === ""; + const isSecretEmpty = secret?.isEmpty; return ( )} - {secret?.valueOverride && ( + {secret?.idOverride && ( @@ -266,11 +273,13 @@ export const SecretOverviewTableRow = ({ secretPath={secretPath} isVisible={isSecretVisible} secretName={secretKey} + isEmpty={secret?.isEmpty} secretValueHidden={secret?.secretValueHidden || false} defaultValue={getDefaultValue(secret, importedSecret)} secretId={secret?.id} - isOverride={Boolean(secret?.valueOverride)} + isOverride={Boolean(secret?.idOverride)} isImportedSecret={isImportedSecret} + importedSecret={importedSecret} isCreatable={isCreatable} onSecretDelete={onSecretDelete} onSecretCreate={onSecretCreate} 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 8c97b4f5c..b90edef1d 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 @@ -25,8 +25,10 @@ import { Tooltip, Tr } from "@app/components/v2"; +import { useProject } from "@app/context"; import { reverseTruncate } from "@app/helpers/reverseTruncate"; import { useTimedReset } from "@app/hooks"; +import { fetchSecretValue } from "@app/hooks/api/dashboard/queries"; import { TDashboardProjectSecretsQuickSearch } from "@app/hooks/api/dashboard/types"; import { ProjectEnv } from "@app/hooks/api/projects/types"; @@ -53,6 +55,8 @@ export const QuickSearchSecretItem = ({ initialState: false }); + const { currentProject } = useProject(); + const [groupSecret] = secretGroup; const handleNavigate = () => { @@ -67,14 +71,29 @@ export const QuickSearchSecretItem = ({ onClose(); }; - const handleCopy = (value: string, env: string) => { - navigator.clipboard.writeText(value); - createNotification({ - type: "info", - title: isSingleEnv ? "Secret value copied." : `Secret value copied from ${env}.`, - text: "" - }); - setIsUrlCopied(true); + const handleCopy = async (env: string) => { + try { + const data = await fetchSecretValue({ + environment: groupSecret.env, + secretPath: groupSecret.path!, + secretKey: groupSecret.key, + projectId: currentProject.id + }); + + navigator.clipboard.writeText(data.valueOverride ?? data.value!); + createNotification({ + type: "info", + title: isSingleEnv ? "Secret value copied." : `Secret value copied from ${env}.`, + text: "" + }); + setIsUrlCopied(true); + } catch (error) { + console.error(error); + createNotification({ + type: "error", + text: "Error fetching secret value" + }); + } }; const secretGroupTags = secretGroup.flatMap((secret) => secret.tags); @@ -146,7 +165,7 @@ export const QuickSearchSecretItem = ({ e.stopPropagation(); const el = envSlugMap.get(groupSecret.env)?.name; if (el) { - handleCopy(groupSecret.value!, el); + handleCopy(el); } }} > @@ -173,7 +192,7 @@ export const QuickSearchSecretItem = ({ e.stopPropagation(); const el = envSlugMap.get(secret.env)?.name; if (el) { - handleCopy(secret.value!, el); + handleCopy(el); } }} key={secret.id} @@ -214,7 +233,7 @@ export const QuickSearchSecretItem = ({ e.stopPropagation(); const el = envSlugMap.get(secret.env)?.name; if (el) { - handleCopy(secret.value!, el); + handleCopy(el); } }} key={secret.id} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index c926450eb..953353e5f 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -183,17 +183,6 @@ const Page = () => { }) ); - const canReadSecretValue = hasSecretReadValueOrDescribePermission( - permission, - ProjectPermissionSecretActions.ReadValue, - { - environment, - secretPath, - secretName: "*", - secretTags: ["*"] - } - ); - const canReadSecretImports = permission.can( ProjectPermissionActions.Read, subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) @@ -271,7 +260,6 @@ const Page = () => { orderDirection, includeImports: canReadSecretImports && (isResourceTypeFiltered ? filter.include.import : true), includeFolders: isResourceTypeFiltered ? filter.include.folder : true, - viewSecretValue: canReadSecretValue, includeDynamicSecrets: canReadDynamicSecret && (isResourceTypeFiltered ? filter.include.dynamic : true), includeSecrets: canReadSecret && (isResourceTypeFiltered ? filter.include.secret : true), @@ -601,7 +589,9 @@ const Page = () => { return secrets; } - const mergedSecrets = [...(secrets || [])]; + const mergedSecrets = [...(secrets || [])] as (SecretV3RawSanitized & { + originalKey?: string; + })[]; pendingChanges.secrets.forEach((change) => { switch (change.type) { @@ -652,7 +642,8 @@ const Page = () => { updatedAt: new Date().toISOString(), __v: 0 })) || [] - : mergedSecrets[updateIndex].tags + : mergedSecrets[updateIndex].tags, + originalKey: mergedSecrets[updateIndex].key }; } break; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/EnvironmentTabs/EnvironmentTabs.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/EnvironmentTabs/EnvironmentTabs.tsx index c9a2c1b62..379708fdc 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/EnvironmentTabs/EnvironmentTabs.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/EnvironmentTabs/EnvironmentTabs.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { faArrowRightArrowLeft, faEllipsisH, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { faEllipsisH, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useQueryClient } from "@tanstack/react-query"; import { useNavigate, useParams } from "@tanstack/react-router"; @@ -199,14 +199,15 @@ export const EnvironmentTabs = ({ secretPath }: Props) => { )} - {currentProject.environments.length > 1 && ( + {/* scott: removing until we have time to update for fetching secret value */} + {/* {currentProject.environments.length > 1 && (
Compare Environments
- )} + )} */} void; environment: string; @@ -35,7 +37,10 @@ type Props = { importedSecrets: { key: string; value?: string; - overriden: { env: string; secretPath: string }; + overridden: { env: string; secretPath: string }; + environment: string; + secretPath?: string; + isEmpty?: boolean; }[]; searchTerm: string; onExpandReplicateSecrets: (id: string) => void; @@ -317,18 +322,11 @@ export const SecretImportItem = ({ )} - {filteredImportedSecrets.map(({ key, value }, index) => ( - - - {key} - - - - - {/* - - */} - + {filteredImportedSecrets.map((secret, index) => ( + ))} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportListView.tsx index d99295286..7fbbc9436 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportListView.tsx @@ -64,18 +64,24 @@ export const computeImportedSecretRows = ( const importedSecretEntries: { key: string; value?: string; - overriden: { + environment: string; + secretPath?: string; + overridden: { env: string; secretPath: string; }; + isEmpty?: boolean; }[] = []; - importedSec.secrets.forEach(({ key, value }) => { + importedSec.secrets.forEach(({ key, value, env, path, isEmpty }) => { if (!importedEntry[key]) { importedSecretEntries.push({ key, value, - overriden: overridenSec?.[key] + environment: env, + secretPath: path, + overridden: overridenSec?.[key], + isEmpty }); importedEntry[key] = true; } diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportSecretRow.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportSecretRow.tsx new file mode 100644 index 000000000..dcebb0ea5 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportSecretRow.tsx @@ -0,0 +1,71 @@ +import { SecretInput } from "@app/components/v2"; +import { useProject } from "@app/context"; +import { useToggle } from "@app/hooks"; +import { useGetSecretValue } from "@app/hooks/api/dashboard/queries"; +import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem"; + +type SecretImportSecretRowProps = { + secret: { + key: string; + value?: string; + environment: string; + secretPath?: string; + isEmpty?: boolean; + overridden: { env: string; secretPath: string }; + }; +}; + +export const SecretImportSecretRow = ({ + secret: { key, environment, secretPath = "/", isEmpty } +}: SecretImportSecretRowProps) => { + const [isFieldFocused, setIsFieldFocused] = useToggle(); + + const { currentProject } = useProject(); + + const canFetchSecretValue = !isEmpty; + + const { + data: secretValue, + isPending: isPendingSecretValue, + isError: isErrorFetchingSecretValue + } = useGetSecretValue( + { + environment, + secretPath, + secretKey: key, + projectId: currentProject.id + }, + { + enabled: isFieldFocused && canFetchSecretValue + } + ); + + const isLoadingSecretValue = canFetchSecretValue && isPendingSecretValue; + + const getValue = () => { + if (isLoadingSecretValue) return HIDDEN_SECRET_VALUE; + + if (isErrorFetchingSecretValue) return "Error loading secret value"; + + return secretValue?.value || ""; + }; + + return ( + + + {key} + + + setIsFieldFocused.on()} + onBlur={() => setIsFieldFocused.off()} + isReadOnly + /> + + {/* + + */} + + ); +}; 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 f00e1b72b..fa839be84 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -1,28 +1,22 @@ import { useEffect } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; import { subject } from "@casl/ability"; -import { faCircleQuestion, faEye } from "@fortawesome/free-regular-svg-icons"; +import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons"; import { - faArrowRotateRight, faCheckCircle, faCopy, - faDesktop, - faEyeSlash, faPlus, faProjectDiagram, faSearch, - faServer, faShare, faTag, faTrash, - faTriangleExclamation, - 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 { useQueryClient } from "@tanstack/react-query"; +import { Link } from "@tanstack/react-router"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; @@ -60,9 +54,13 @@ import { } from "@app/context"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { getProjectBaseURL } from "@app/helpers/project"; -import { usePopUp } from "@app/hooks"; +import { usePopUp, useToggle } from "@app/hooks"; import { useGetSecretVersion } from "@app/hooks/api"; -import { ActorType } from "@app/hooks/api/auditLogs/enums"; +import { + dashboardKeys, + fetchSecretValue, + useGetSecretValue +} from "@app/hooks/api/dashboard/queries"; import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries"; import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types"; import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; @@ -70,6 +68,7 @@ import { camelCaseToSpaces } from "@app/lib/fn/string"; import { HIDDEN_SECRET_VALUE } from "./SecretItem"; import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; +import { SecretVersionItem } from "./SecretVersionItem"; type Props = { isOpen?: boolean; @@ -77,7 +76,7 @@ type Props = { secretPath: string; onToggle: (isOpen: boolean) => void; onClose: () => void; - secret: SecretV3RawSanitized; + secret: SecretV3RawSanitized & { originalKey?: string }; onDeleteSecret: () => void; onSaveSecret: ( orgSec: SecretV3RawSanitized, @@ -92,7 +91,7 @@ type Props = { export const SecretDetailSidebar = ({ isOpen, onToggle, - secret, + secret: originalSecret, onDeleteSecret, onSaveSecret, tags, @@ -101,16 +100,90 @@ export const SecretDetailSidebar = ({ secretPath, handleSecretShare }: Props) => { + const { currentProject } = useProject(); + const [isFieldFocused, setIsFieldFocused] = useToggle(); + const queryClient = useQueryClient(); + + const canFetchSecretValue = + Boolean(originalSecret) && !originalSecret.secretValueHidden && !originalSecret.isEmpty; + + const fetchSecretValueParams = { + environment, + secretPath, + secretKey: originalSecret?.originalKey || originalSecret?.key, + projectId: currentProject.id, + isOverride: Boolean(originalSecret?.idOverride) + }; + + const { + data: secretValueData, + isPending: isPendingSecretValue, + isError: isErrorFetchingSecretValue + } = useGetSecretValue(fetchSecretValueParams, { + enabled: canFetchSecretValue && isFieldFocused + }); + + const isLoadingSecretValue = canFetchSecretValue && isPendingSecretValue; + const hasFetchedSecretValue = !canFetchSecretValue || Boolean(secretValueData); + + const secret = { + ...originalSecret, + value: originalSecret?.value ?? secretValueData?.value, + valueOverride: originalSecret?.valueOverride ?? secretValueData?.valueOverride + }; + + const { permission } = useProjectPermission(); + + const canEditSecretValue = permission.can( + ProjectPermissionSecretActions.Edit, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: secret.key, + secretTags: ["*"] + }) + ); + + const getDefaultValue = () => { + if (isLoadingSecretValue) return HIDDEN_SECRET_VALUE; + + if (secret.secretValueHidden) { + return canEditSecretValue ? HIDDEN_SECRET_VALUE : ""; + } + + if (isErrorFetchingSecretValue) return "Error loading secret value..."; + + return secret.value || ""; + }; + + const getOverrideDefaultValue = () => { + if (isLoadingSecretValue) return HIDDEN_SECRET_VALUE; + + if (secret.secretValueHidden) { + return canEditSecretValue ? HIDDEN_SECRET_VALUE : ""; + } + + if (isErrorFetchingSecretValue) return "Error loading secret value..."; + + return secret.valueOverride || ""; + }; + const { control, watch, handleSubmit, setValue, reset, + getValues, + getFieldState, formState: { isDirty } } = useForm({ resolver: zodResolver(formSchema), - values: secret, + values: { + ...secret, + valueOverride: getOverrideDefaultValue(), + value: getDefaultValue() + }, disabled: !secret }); @@ -119,9 +192,6 @@ export const SecretDetailSidebar = ({ "secretReferenceTree" ] as const); - const { permission } = useProjectPermission(); - const { currentProject } = useProject(); - const tagFields = useFieldArray({ control, name: "tags" @@ -139,7 +209,6 @@ export const SecretDetailSidebar = ({ {} ); const selectTagSlugs = selectedTags.map((i) => i.slug); - const navigate = useNavigate(); const cannotEditSecret = permission.cannot( ProjectPermissionSecretActions.Edit, @@ -205,7 +274,16 @@ export const SecretDetailSidebar = ({ }; const handleFormSubmit = async (data: TFormSchema) => { - await onSaveSecret(secret, { ...secret, ...data }, () => reset()); + await onSaveSecret( + secret, + { + ...secret, + ...data, + value: getFieldState("value").isDirty ? data.value : undefined, + valueOverride: getFieldState("valueOverride").isDirty ? data.valueOverride : undefined + }, + () => reset() + ); }; useEffect(() => { @@ -218,58 +296,22 @@ export const SecretDetailSidebar = ({ ); }, [secret?.secretReminderRecipients]); - const getModifiedByIcon = (userType: string | undefined | null) => { - switch (userType) { - case ActorType.USER: - return faUser; - case ActorType.IDENTITY: - return faDesktop; - default: - return faServer; - } - }; + const fetchValue = async () => { + if (secretValueData) return secretValueData.valueOverride ?? secretValueData.value; - const getModifiedByName = ( - userType: string | undefined | null, - userName: string | null | undefined - ) => { - switch (userType) { - case ActorType.PLATFORM: - return "System-generated"; - case ActorType.IDENTITY: - return userName || "Deleted Identity"; - case ActorType.USER: - return userName || "Deleted User"; - default: - return "Unknown"; - } - }; + try { + const data = await fetchSecretValue(fetchSecretValueParams); - const getLinkToModifyHistoryEntity = ( - actorId: string, - actorType: string, - membershipId: string | null = "" - ) => { - switch (actorType) { - case ActorType.USER: - return `/projects/secret-management/${currentProject.id}/members/${membershipId}`; - case ActorType.IDENTITY: - return `/projects/secret-management/${currentProject.id}/identities/${actorId}`; - default: - return null; - } - }; + queryClient.setQueryData(dashboardKeys.getSecretValue(fetchSecretValueParams), data); - const onModifyHistoryClick = ( - actorId: string | undefined | null, - actorType: string | undefined | null, - membershipId: string | undefined | null - ) => { - if (actorType && actorId && actorType !== ActorType.PLATFORM) { - const redirectLink = getLinkToModifyHistoryEntity(actorId, actorType, membershipId); - if (redirectLink) { - navigate({ to: redirectLink }); - } + return data?.valueOverride ?? data.value; + } catch (error) { + console.error(error); + createNotification({ + type: "error", + text: "Error fetching secret value" + }); + throw error; } }; @@ -313,6 +355,7 @@ export const SecretDetailSidebar = ({ title={`Secret – ${secret?.key}`} className="thin-scrollbar h-full" cardBodyClassName="pb-0" + onOpenAutoFocus={(e) => e.preventDefault()} >
setIsFieldFocused.on()} + onBlur={() => { + setIsFieldFocused.off(); + field.onBlur(); + }} /> } - onClick={() => { - const value = secret?.valueOverride ?? secret?.value; - if (value) { - handleSecretShare(value); + onClick={async () => { + let value: string | undefined; + + if (hasFetchedSecretValue) { + const values = getValues(["value", "valueOverride"]); + value = secret.idOverride ? values[1] : values[0]; + } else { + value = await fetchValue(); } + + handleSecretShare(value ?? ""); }} > Share @@ -644,179 +704,20 @@ export const SecretDetailSidebar = ({
Version History
- {secretVersion?.map( - ({ createdAt, secretValue, secretValueHidden, version, id, actor }) => ( -
-
-
-
-
- v{version} -
-
-
{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" - > - -
-
-
-
- )} -
-
- Value: -
-
-
- - -
- - {secretValueHidden - ? HIDDEN_SECRET_VALUE - : secretValue?.replace(/./g, "*")} - - -
-
-
-
-
- {!secret?.isRotatedSecret && ( -
- - setValue("value", secretValue, { shouldDirty: true })} - > - - - -
- )} -
- ) - )} + setTimeout(() => { + setValue("value", versionValue, { shouldDirty: true }); + }, 5); + }} + /> + ))}
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 98b5e780c..b8f37ff32 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx @@ -27,8 +27,8 @@ import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { ProjectPermissionActions, ProjectPermissionSub, - useProjectPermission, - useProject + useProject, + useProjectPermission } from "@app/context"; import { usePopUp, useToggle } from "@app/hooks"; import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; @@ -47,6 +47,13 @@ import { faEyeSlash, faKey, faRotate } from "@fortawesome/free-solid-svg-icons"; import { PendingAction } from "@app/hooks/api/secretFolders/types"; import { format } from "date-fns"; import { CreateReminderForm } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/CreateReminderForm"; +import { + dashboardKeys, + fetchSecretValue, + useGetSecretValue +} from "@app/hooks/api/dashboard/queries"; +import { createNotification } from "@app/components/notifications"; +import { useQueryClient } from "@tanstack/react-query"; import { FontAwesomeSpriteName, formSchema, @@ -56,11 +63,11 @@ import { import { CollapsibleSecretImports } from "./CollapsibleSecretImports"; import { useBatchModeActions } from "../../SecretMainPage.store"; -export const HIDDEN_SECRET_VALUE = "*****************************"; +export const HIDDEN_SECRET_VALUE = "*************************"; export const HIDDEN_SECRET_VALUE_API_MASK = ""; type Props = { - secret: SecretV3RawSanitized; + secret: SecretV3RawSanitized & { originalKey?: string }; onSaveSecret: ( orgSec: SecretV3RawSanitized, modSec: Omit & { tags?: { id: string }[] }, @@ -91,7 +98,7 @@ type Props = { export const SecretItem = memo( ({ - secret, + secret: originalSecret, onSaveSecret, onDeleteSecret, onDetailViewSecret, @@ -114,9 +121,41 @@ export const SecretItem = memo( ] as const); const { currentProject } = useProject(); const { permission } = useProjectPermission(); - const { isRotatedSecret } = secret; const { removePendingChange } = useBatchModeActions(); + const [isFieldFocused, setIsFieldFocused] = useToggle(); + const queryClient = useQueryClient(); + + const canFetchSecretValue = + !originalSecret.secretValueHidden && !originalSecret.isEmpty && !isPending; + + const fetchSecretValueParams = { + environment, + secretPath, + secretKey: originalSecret.originalKey || originalSecret.key, + projectId: currentProject.id, + isOverride: Boolean(originalSecret.idOverride) + }; + + const { + data: secretValueData, + isPending: isPendingSecretValueData, + isError: isErrorFetchingSecretValue + } = useGetSecretValue(fetchSecretValueParams, { + enabled: canFetchSecretValue && (isVisible || isFieldFocused) + }); + + const isLoadingSecretValue = canFetchSecretValue && isPendingSecretValueData; + const hasFetchedSecretValue = !canFetchSecretValue || Boolean(secretValueData); + + const secret = { + ...originalSecret, + value: originalSecret.value ?? secretValueData?.value, + valueOverride: originalSecret.valueOverride ?? secretValueData?.valueOverride + }; + + const { isRotatedSecret } = secret; + const autoSaveTimeoutRef = useRef(); const isAutoSavingRef = useRef(false); @@ -139,12 +178,43 @@ export const SecretItem = memo( ); const getDefaultValue = () => { + if (isLoadingSecretValue) return HIDDEN_SECRET_VALUE; + if (secret.secretValueHidden && !isPending) { return canEditSecretValue ? HIDDEN_SECRET_VALUE : ""; } - return secret.valueOverride || secret.value || ""; + + if (isErrorFetchingSecretValue) return "Error loading secret value..."; + + return secret.value || ""; }; + const getOverrideDefaultValue = () => { + if (isLoadingSecretValue) return HIDDEN_SECRET_VALUE; + + if (secret.secretValueHidden && !isPending) { + return canEditSecretValue ? HIDDEN_SECRET_VALUE : ""; + } + + if (isErrorFetchingSecretValue) return "Error loading secret value..."; + + return secret.valueOverride || ""; + }; + + const formMethods = useForm({ + defaultValues: { + ...secret, + valueOverride: getOverrideDefaultValue(), + value: getDefaultValue() + }, + values: { + ...secret, + valueOverride: getOverrideDefaultValue(), + value: getDefaultValue() + }, + resolver: zodResolver(formSchema) + }); + const { handleSubmit, control, @@ -155,17 +225,7 @@ export const SecretItem = memo( getValues, trigger, formState: { isDirty, isSubmitting, errors } - } = useForm({ - defaultValues: { - ...secret, - value: getDefaultValue() - }, - values: { - ...secret, - value: getDefaultValue() - }, - resolver: zodResolver(formSchema) - }); + } = formMethods; const secretName = watch("key"); const overrideAction = watch("overrideAction"); @@ -255,7 +315,10 @@ export const SecretItem = memo( ); const isReadOnlySecret = - isReadOnly || isRotatedSecret || (isPending && pendingAction === PendingAction.Delete); + isReadOnly || + isRotatedSecret || + (isPending && pendingAction === PendingAction.Delete) || + isLoadingSecretValue; const { secretValueHidden } = secret; @@ -322,12 +385,36 @@ export const SecretItem = memo( } }; - const copyTokenToClipboard = () => { - const [overrideValue, value] = getValues(["value", "valueOverride"]); - if (isOverridden) { - navigator.clipboard.writeText(value as string); + const fetchValue = async () => { + if (secretValueData) return secretValueData; + + try { + const data = await fetchSecretValue(fetchSecretValueParams); + + queryClient.setQueryData(dashboardKeys.getSecretValue(fetchSecretValueParams), data); + + return data; + } catch (e) { + console.error(e); + createNotification({ + type: "error", + text: "Failed to fetch secret value" + }); + throw e; + } + }; + + const copyTokenToClipboard = async () => { + if (hasFetchedSecretValue) { + const [overrideValue, value] = getValues(["value", "valueOverride"]); + if (isOverridden) { + navigator.clipboard.writeText(value as string); + } else { + navigator.clipboard.writeText(overrideValue as string); + } } else { - navigator.clipboard.writeText(overrideValue as string); + const data = await fetchValue(); + navigator.clipboard.writeText((data.valueOverride ?? data.value) as string); } setIsSecValueCopied.on(); }; @@ -428,6 +515,11 @@ export const SecretItem = memo( isVisible={isVisible} isReadOnly={isReadOnly} {...field} + onFocus={() => setIsFieldFocused.on()} + onBlur={() => { + setIsFieldFocused.off(); + field.onBlur(); + }} containerClassName="py-1.5 rounded-md transition-all" /> )} @@ -446,6 +538,13 @@ export const SecretItem = memo( environment={environment} secretPath={secretPath} {...field} + onFocus={() => { + setIsFieldFocused.on(); + }} + onBlur={() => { + setIsFieldFocused.off(); + field.onBlur(); + }} defaultValue={ secretValueHidden && !isPending ? HIDDEN_SECRET_VALUE : undefined } @@ -682,7 +781,19 @@ export const SecretItem = memo( variant="plain" size="md" ariaLabel="share-secret" - onClick={() => onShareSecret(secret)} + onClick={async () => { + if (hasFetchedSecretValue) { + onShareSecret(secret); + return; + } + + const data = await fetchValue(); + + onShareSecret({ + ...secret, + ...data + }); + }} > } /> - handlePopUpToggle("secretDetail", isOpen)} - secret={popUp.secretDetail.data as SecretV3RawSanitized} - onDeleteSecret={() => handlePopUpOpen("deleteSecret", popUp.secretDetail.data)} - onClose={() => handlePopUpClose("secretDetail")} - onSaveSecret={handleSaveSecret} - tags={wsTags} - onCreateTag={() => handlePopUpOpen("createTag")} - handleSecretShare={(value: string) => handlePopUpOpen("createSharedSecret", { value })} - /> + {popUp.secretDetail.data && ( + handlePopUpToggle("secretDetail", isOpen)} + secret={popUp.secretDetail.data as SecretV3RawSanitized} + onDeleteSecret={() => handlePopUpOpen("deleteSecret", popUp.secretDetail.data)} + onClose={() => handlePopUpClose("secretDetail")} + onSaveSecret={handleSaveSecret} + tags={wsTags} + onCreateTag={() => handlePopUpOpen("createTag")} + handleSecretShare={(value: string) => handlePopUpOpen("createSharedSecret", { value })} + /> + )} handlePopUpToggle("createTag", isOpen)} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretVersionItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretVersionItem.tsx new file mode 100644 index 000000000..2d3e123a0 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretVersionItem.tsx @@ -0,0 +1,286 @@ +import { useState } from "react"; +import { faEye } from "@fortawesome/free-regular-svg-icons"; +import { + faArrowRotateRight, + faDesktop, + faEyeSlash, + faServer, + faUser +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate } from "@tanstack/react-router"; +import { format } from "date-fns"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +import { IconButton, Tooltip } from "@app/components/v2"; +import { useProject } from "@app/context"; +import { ActorType } from "@app/hooks/api/auditLogs/enums"; +import { fetchSecretVersionValue } from "@app/hooks/api/secrets/queries"; +import { SecretV3RawSanitized, SecretVersions } from "@app/hooks/api/secrets/types"; + +interface SecretVersionItemProps { + secretVersion: SecretVersions; + secret: SecretV3RawSanitized; + currentVersion: number; + onRevert: (secretValue: string) => void; +} + +export const SecretVersionItem = ({ + secretVersion: { createdAt, version, actor, secretValueHidden }, + secret, + currentVersion, + onRevert +}: SecretVersionItemProps) => { + const { currentProject } = useProject(); + + const navigate = useNavigate(); + + const getModifiedByIcon = (userType: string | undefined | null) => { + switch (userType) { + case ActorType.USER: + return faUser; + case ActorType.IDENTITY: + return faDesktop; + default: + return faServer; + } + }; + + const getModifiedByName = ( + userType: string | undefined | null, + userName: string | null | undefined + ) => { + switch (userType) { + case ActorType.PLATFORM: + return "System-generated"; + case ActorType.IDENTITY: + return userName || "Deleted Identity"; + case ActorType.USER: + return userName || "Deleted User"; + default: + return "Unknown"; + } + }; + + const getLinkToModifyHistoryEntity = ( + actorId: string, + actorType: string, + membershipId: string | null = "" + ) => { + switch (actorType) { + case ActorType.USER: + return `/projects/secret-management/${currentProject.id}/members/${membershipId}`; + case ActorType.IDENTITY: + return `/projects/secret-management/${currentProject.id}/identities/${actorId}`; + default: + return null; + } + }; + + const onModifyHistoryClick = ( + actorId: string | undefined | null, + actorType: string | undefined | null, + membershipId: string | undefined | null + ) => { + if (actorType && actorId && actorType !== ActorType.PLATFORM) { + const redirectLink = getLinkToModifyHistoryEntity(actorId, actorType, membershipId); + if (redirectLink) { + navigate({ to: redirectLink }); + } + } + }; + + const [secretValue, setSecretValue] = useState(null); + const [isFetchingValue, setIsFetchingValue] = useState(false); + const handleGetSecretValue = async () => { + if (secretValue) return secretValue; + try { + setIsFetchingValue(true); + const value = await fetchSecretVersionValue(secret.id, version); + setSecretValue(value); + return value; + } catch (e) { + console.error(e); + createNotification({ + type: "error", + text: "Failed to fetch secret version value" + }); + throw e; + } finally { + setIsFetchingValue(false); + } + }; + + return ( +
+
+
+
+
+ v{version} +
+
+
{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" + > + +
+
+
+
+ )} +
+
+ Value: +
+
+
+ + +
+ + **** + + +
+
+
+
+
+ {!secret?.isRotatedSecret && ( +
+ + { + if (secretValue) { + onRevert(secretValue); + return; + } + + const value = await handleGetSecretValue(); + + onRevert(value); + }} + > + + + +
+ )} +
+ ); +}; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx index 77e6eddab..a2c54a11f 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx @@ -15,13 +15,13 @@ import { twMerge } from "tailwind-merge"; import { ProjectPermissionCan } from "@app/components/permissions"; import { SecretRotationV2StatusBadge } from "@app/components/secret-rotations-v2/SecretRotationV2StatusBadge"; import { IconButton, Modal, ModalContent, TableContainer, Tag, Tooltip } from "@app/components/v2"; -import { Blur } from "@app/components/v2/Blur"; -import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types"; import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2"; import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2"; +import { SecretRotationSecretRow } from "./SecretRotationSecretRow"; + type Props = { secretRotation: TSecretRotationV2; onEdit: () => void; @@ -209,43 +209,13 @@ export const SecretRotationItem = ({ {secrets.map((secret, index) => { return ( - - - - - {secret?.key ?? "********"} - - - - {/* eslint-disable-next-line no-nested-ternary */} - {!secret ? ( -
********
- ) : secret.secretValueHidden ? ( - - ) : ( - {}} - /> - )} - - -
+ ); })} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationSecretRow.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationSecretRow.tsx new file mode 100644 index 000000000..cf89c3296 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationSecretRow.tsx @@ -0,0 +1,76 @@ +import { twMerge } from "tailwind-merge"; + +import { SecretInput, Tooltip } from "@app/components/v2"; +import { Blur } from "@app/components/v2/Blur"; +import { useProject } from "@app/context"; +import { useToggle } from "@app/hooks"; +import { useGetSecretValue } from "@app/hooks/api/dashboard/queries"; +import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2"; +import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem"; + +interface SecretRotationSecretRowProps { + secret: TSecretRotationV2["secrets"][number]; + secretPath: string; + environment: string; +} + +export const SecretRotationSecretRow = ({ + secret, + environment, + secretPath +}: SecretRotationSecretRowProps) => { + const [isFieldFocused, setIsFieldFocused] = useToggle(); + + const { currentProject } = useProject(); + + const { data: secretValue, isPending: isLoadingSecretValue } = useGetSecretValue( + { + environment, + secretPath, + secretKey: secret!.key, + projectId: currentProject.id + }, + { + enabled: isFieldFocused && Boolean(secret) + } + ); + + const getValue = () => { + if (isLoadingSecretValue) return HIDDEN_SECRET_VALUE; + + if (!secretValue) return "Error loading secret value"; + + return secretValue.value || ""; + }; + + return ( + + + + {secret?.key ?? "********"} + + + {/* eslint-disable-next-line no-nested-ternary */} + {!secret ? ( +
********
+ ) : secret.secretValueHidden ? ( + + ) : ( + setIsFieldFocused.on()} + onBlur={() => setIsFieldFocused.off()} + /> + )} + + +
+ ); +}; From 5a4b83d90065c0fac8a321d4a4f11743269321a2 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Wed, 17 Sep 2025 18:36:15 -0700 Subject: [PATCH 02/10] improvements: address feedback progress --- .../secret-v2-bridge/secret-v2-bridge-dal.ts | 7 ++-- .../SecretReferenceDetails.tsx | 8 +++++ frontend/src/hooks/api/secrets/queries.tsx | 1 + .../SecretOverviewTableRow/SecretEditRow.tsx | 28 ++++++++-------- .../SecretListView/SecretDetailSidebar.tsx | 32 ++++++------------- 5 files changed, 35 insertions(+), 41 deletions(-) diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index 93afb3b55..d8220e4f5 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -446,9 +446,10 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { } }) .where((bd) => { - void bd - .whereNull(`${TableName.SecretV2}.userId`) - .orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null }); + void bd.whereNull(`${TableName.SecretV2}.userId`); + // scott: removing this as we don't need to count overrides + // and there is currently a bug when you move secrets that doesn't move the override so this can skew count + // .orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null }); }) .countDistinct(`${TableName.SecretV2}.key`); diff --git a/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx b/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx index ada741145..d4f09ea8f 100644 --- a/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx +++ b/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx @@ -130,6 +130,14 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop ); } + if (tree?.children?.length === 0) { + return ( +
+ This secret does not contain references +
+ ); + } + return (
diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index b12cca38b..e3cbf7f0e 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -119,6 +119,7 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => { sec.valueOverride = personalSecret.value; sec.overrideAction = "modified"; sec.isEmpty = personalSecret.isEmpty; + sec.secretValueHidden = false; } }); 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 84b58fb7f..be914024e 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { subject } from "@casl/ability"; import { @@ -15,10 +15,7 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; -import { - hasSecretReference, - SecretReferenceTree -} from "@app/components/secrets/SecretReferenceDetails"; +import { SecretReferenceTree } from "@app/components/secrets/SecretReferenceDetails"; import { DeleteActionModal, IconButton, @@ -150,13 +147,20 @@ export const SecretEditRow = ({ control, reset, getValues, + setValue, formState: { isDirty, isSubmitting } } = useForm({ - values: { + defaultValues: { value: (secretValueData?.valueOverride ?? secretValueData?.value) || null } }); + useEffect(() => { + if (secretValueData && !isDirty) { + setValue("value", secretValueData.valueOverride ?? secretValueData.value); + } + }, [secretValueData]); + const { permission } = useProjectPermission(); const [isDeleting, setIsDeleting] = useToggle(); @@ -171,7 +175,7 @@ export const SecretEditRow = ({ }; const handleCopySecretToClipboard = async () => { - if (!isSecretValueFetched) { + if (!isSecretValueFetched && !isDirty) { try { const data = await fetchSecretValue(fetchSecretValueParams); @@ -402,18 +406,12 @@ export const SecretEditRow = ({
- + 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 fa839be84..c0ba7184e 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -21,10 +21,7 @@ import { Link } from "@tanstack/react-router"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; -import { - hasSecretReference, - SecretReferenceTree -} from "@app/components/secrets/SecretReferenceDetails"; +import { SecretReferenceTree } from "@app/components/secrets/SecretReferenceDetails"; import { Button, Drawer, @@ -843,26 +840,15 @@ export const SecretDetailSidebar = ({ )}
- } + onClick={() => handlePopUpOpen("secretReferenceTree", secretKey)} > -
- -
-
+ Secret Reference Tree + Date: Wed, 17 Sep 2025 19:39:21 -0700 Subject: [PATCH 03/10] improvements: address feedback 2 --- backend/src/server/routes/v4/secret-router.ts | 17 ++++++++++++- .../secret-v2-bridge-service.ts | 2 +- .../components/SecretListView/SecretItem.tsx | 25 +++++++++---------- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/backend/src/server/routes/v4/secret-router.ts b/backend/src/server/routes/v4/secret-router.ts index ead96668c..88cf62902 100644 --- a/backend/src/server/routes/v4/secret-router.ts +++ b/backend/src/server/routes/v4/secret-router.ts @@ -1262,7 +1262,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { handler: async (req) => { const { secretName } = req.params; const { secretPath, environment, projectId } = req.query; - const { tree, value } = await server.services.secret.getSecretReferenceTree({ + const { tree, value, secret } = await server.services.secret.getSecretReferenceTree({ actorId: req.permission.id, actor: req.permission.type, actorAuthMethod: req.permission.authMethod, @@ -1273,6 +1273,21 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { environment }); + await server.services.auditLog.createAuditLog({ + projectId, + ...req.auditLogInfo, + event: { + type: EventType.GET_SECRET, + metadata: { + environment, + secretPath, + secretId: secret.id, + secretKey: secretName, + secretVersion: secret.version + } + } + }); + return { tree, value }; } }); 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 2ed543c81..b3c8d3947 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 @@ -2941,7 +2941,7 @@ export const secretV2BridgeServiceFactory = ({ secretKey: secretName }); - return { tree: stackTrace, value: expandedValue }; + return { tree: stackTrace, value: expandedValue, secret }; }; const getAccessibleSecrets = async ({ 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 b8f37ff32..4b75e84e8 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx @@ -127,6 +127,7 @@ export const SecretItem = memo( const queryClient = useQueryClient(); const canFetchSecretValue = + // TODO NOTE FOR PR PROGRESS: may need to remove !isPending, messes with key edits not fetching proper value !originalSecret.secretValueHidden && !originalSecret.isEmpty && !isPending; const fetchSecretValueParams = { @@ -201,7 +202,17 @@ export const SecretItem = memo( return secret.valueOverride || ""; }; - const formMethods = useForm({ + const { + handleSubmit, + control, + register, + watch, + setValue, + reset, + getValues, + trigger, + formState: { isDirty, isSubmitting, errors } + } = useForm({ defaultValues: { ...secret, valueOverride: getOverrideDefaultValue(), @@ -215,18 +226,6 @@ export const SecretItem = memo( resolver: zodResolver(formSchema) }); - const { - handleSubmit, - control, - register, - watch, - setValue, - reset, - getValues, - trigger, - formState: { isDirty, isSubmitting, errors } - } = formMethods; - const secretName = watch("key"); const overrideAction = watch("overrideAction"); const hasComment = Boolean(watch("comment")); From f1f391f89b2c6cc4a744f83e418deb0d7b3f373f Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 18 Sep 2025 09:25:34 -0700 Subject: [PATCH 04/10] improvements: fix commit changes with value retrieval --- .../InfisicalSecretInput.tsx | 2 ++ .../components/v2/SecretInput/SecretInput.tsx | 21 ++++++++++++++++--- .../SecretMainPage.store.tsx | 2 +- .../components/CommitForm/CommitForm.tsx | 2 +- .../components/SecretListView/SecretItem.tsx | 16 +++++++------- 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx index 8c26dfc81..347be1b4f 100644 --- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx +++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx @@ -55,6 +55,8 @@ type Props = Omit, "onChange" | "val secretPath?: string; environment?: string; containerClassName?: string; + isLoadingValue?: boolean; + isErrorLoadingValue?: boolean; }; type ReferenceItem = { diff --git a/frontend/src/components/v2/SecretInput/SecretInput.tsx b/frontend/src/components/v2/SecretInput/SecretInput.tsx index c320bb0af..60ab5848f 100644 --- a/frontend/src/components/v2/SecretInput/SecretInput.tsx +++ b/frontend/src/components/v2/SecretInput/SecretInput.tsx @@ -7,7 +7,16 @@ import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPa const REGEX = /(\${([a-zA-Z0-9-_.]+)})/g; -const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?: boolean) => { +const syntaxHighlight = ( + content?: string | null, + isVisible?: boolean, + isImport?: boolean, + isLoadingValue?: boolean, + isErrorLoadingValue?: boolean +) => { + if (isLoadingValue) return HIDDEN_SECRET_VALUE; + if (isErrorLoadingValue) + return Error loading secret value.; if (isImport && !content) return "IMPORTED"; if (content === "") return "EMPTY"; if (!content) return "EMPTY"; @@ -48,6 +57,8 @@ type Props = TextareaHTMLAttributes & { isDisabled?: boolean; containerClassName?: string; canEditButNotView?: boolean; + isLoadingValue?: boolean; + isErrorLoadingValue?: boolean; }; const commonClassName = "font-mono text-sm caret-white border-none outline-none w-full break-all"; @@ -65,6 +76,8 @@ export const SecretInput = forwardRef( isReadOnly, onFocus, canEditButNotView, + isLoadingValue, + isErrorLoadingValue, ...props }, ref @@ -83,7 +96,9 @@ export const SecretInput = forwardRef( {syntaxHighlight( value, isVisible || (isSecretFocused && !valueAlwaysHidden), - isImport + isImport, + isLoadingValue, + isErrorLoadingValue )} @@ -114,7 +129,7 @@ export const SecretInput = forwardRef( }} value={value || ""} {...props} - readOnly={isReadOnly} + readOnly={isReadOnly || isLoadingValue || isErrorLoadingValue} />
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx index 4cccc6480..64367a7d1 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx @@ -408,7 +408,7 @@ const createBatchModeStore: StateCreator const mergedUpdate: PendingSecretUpdate = { ...existingUpdate, secretKey: existingUpdate.secretKey, - originalValue: existingUpdate.originalValue, + originalValue: change.originalValue, originalComment: existingUpdate.originalComment, originalSkipMultilineEncoding: existingUpdate.originalSkipMultilineEncoding, originalTags: existingUpdate.originalTags, diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx index f81cd7b2d..ac72adf43 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx @@ -105,7 +105,7 @@ const RenderSecretChanges = ({ onDiscard, change }: RenderResourceProps) => { version: 1, // placeholder, not used secretKey: change.newSecretName ? existingSecret.key : undefined, secretValue: - change.secretValue !== undefined ? (existingSecret.value ?? "") : undefined, + change.secretValue !== undefined ? (change.originalValue ?? "") : undefined, tags: change.tags ? (existingSecret.tags?.map((tag) => tag.slug) ?? []) : undefined, secretMetadata: change.secretMetadata ? existingSecret.secretMetadata : undefined, skipMultilineEncoding: 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 4b75e84e8..7b3ac9738 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx @@ -126,9 +126,7 @@ export const SecretItem = memo( const [isFieldFocused, setIsFieldFocused] = useToggle(); const queryClient = useQueryClient(); - const canFetchSecretValue = - // TODO NOTE FOR PR PROGRESS: may need to remove !isPending, messes with key edits not fetching proper value - !originalSecret.secretValueHidden && !originalSecret.isEmpty && !isPending; + const canFetchSecretValue = !originalSecret.secretValueHidden && !originalSecret.isEmpty; const fetchSecretValueParams = { environment, @@ -179,25 +177,25 @@ export const SecretItem = memo( ); const getDefaultValue = () => { - if (isLoadingSecretValue) return HIDDEN_SECRET_VALUE; + if (isLoadingSecretValue) return undefined; if (secret.secretValueHidden && !isPending) { return canEditSecretValue ? HIDDEN_SECRET_VALUE : ""; } - if (isErrorFetchingSecretValue) return "Error loading secret value..."; + if (isErrorFetchingSecretValue) return undefined; return secret.value || ""; }; const getOverrideDefaultValue = () => { - if (isLoadingSecretValue) return HIDDEN_SECRET_VALUE; + if (isLoadingSecretValue) return undefined; if (secret.secretValueHidden && !isPending) { return canEditSecretValue ? HIDDEN_SECRET_VALUE : ""; } - if (isErrorFetchingSecretValue) return "Error loading secret value..."; + if (isErrorFetchingSecretValue) return undefined; return secret.valueOverride || ""; }; @@ -510,6 +508,8 @@ export const SecretItem = memo( control={control} render={({ field }) => ( ( Date: Thu, 18 Sep 2025 09:47:44 -0700 Subject: [PATCH 05/10] improvement: remove comments --- .../components/SecretImportListView/SecretImportItem.tsx | 1 - .../components/SecretImportListView/SecretImportSecretRow.tsx | 3 --- 2 files changed, 4 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem.tsx index 485c757e0..59764de78 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem.tsx @@ -304,7 +304,6 @@ export const SecretImportItem = ({ Key Value - {/* Override */} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportSecretRow.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportSecretRow.tsx index dcebb0ea5..fd8c8a440 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportSecretRow.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportSecretRow.tsx @@ -63,9 +63,6 @@ export const SecretImportSecretRow = ({ isReadOnly /> - {/* - - */} ); }; From 999ce3fec60e61fe27be43f42cabda8a3a576417 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 18 Sep 2025 12:09:28 -0700 Subject: [PATCH 06/10] improvements: resolve more commit state issues form value reveal change --- frontend/src/hooks/api/secrets/queries.tsx | 8 +------- .../SecretOverviewRotationSecretRow.tsx | 2 +- .../SecretOverviewTableRow/SecretEditRow.tsx | 2 +- .../components/CommitForm/CommitForm.tsx | 2 +- .../components/SecretListView/SecretItem.tsx | 11 ++++++++--- .../SecretRotationSecretRow.tsx | 2 +- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index e3cbf7f0e..4eb2ba22f 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -272,13 +272,7 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) => export const fetchSecretVersionValue = async (secretId: string, version: number) => { const { data } = await apiRequest.get( - `/api/v1/dashboard/secret-versions/${secretId}/value/${version}`, - { - params: { - secretId, - version - } - } + `/api/v1/dashboard/secret-versions/${secretId}/value/${version}` ); return data.value; }; diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewSecretRotationRow/SecretOverviewRotationSecretRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewSecretRotationRow/SecretOverviewRotationSecretRow.tsx index e8df569b9..f8efdf17e 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewSecretRotationRow/SecretOverviewRotationSecretRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewSecretRotationRow/SecretOverviewRotationSecretRow.tsx @@ -29,7 +29,7 @@ export const SecretOverviewRotationSecretRow = ({ const { data: secretValueData, isError } = useGetSecretValue( { - secretKey: secret!.key, + secretKey: secret?.key ?? "", environment, secretPath, projectId: currentProject.id 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 be914024e..9046f2ea7 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -117,7 +117,7 @@ export const SecretEditRow = ({ ? { environment: importedSecret.environment, secretPath: importedSecret.secretPath, - secretKey: importedSecret.secret!.key, + secretKey: importedSecret.secret?.key ?? "", projectId: currentProject.id } : { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx index ac72adf43..59cceb9bb 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx @@ -143,7 +143,7 @@ const RenderSecretChanges = ({ onDiscard, change }: RenderResourceProps) => { { version: 1, // placeholder, not used secretKey, - secretValue + secretValue: secretValue || undefined } ] }} 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 7b3ac9738..e933d1b5e 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx @@ -126,7 +126,10 @@ export const SecretItem = memo( const [isFieldFocused, setIsFieldFocused] = useToggle(); const queryClient = useQueryClient(); - const canFetchSecretValue = !originalSecret.secretValueHidden && !originalSecret.isEmpty; + const canFetchSecretValue = + !originalSecret.secretValueHidden && + !originalSecret.isEmpty && + pendingAction !== PendingAction.Create; const fetchSecretValueParams = { environment, @@ -508,13 +511,15 @@ export const SecretItem = memo( control={control} render={({ field }) => ( setIsFieldFocused.on()} + onFocus={() => { + if (secret.idOverride) setIsFieldFocused.on(); + }} onBlur={() => { setIsFieldFocused.off(); field.onBlur(); diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationSecretRow.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationSecretRow.tsx index cf89c3296..e8b7788af 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationSecretRow.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationSecretRow.tsx @@ -27,7 +27,7 @@ export const SecretRotationSecretRow = ({ { environment, secretPath, - secretKey: secret!.key, + secretKey: secret?.key ?? "", projectId: currentProject.id }, { From 91d3ed632ae7cacd4b9e046e67fbc2484e0e331c Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 18 Sep 2025 12:17:50 -0700 Subject: [PATCH 07/10] improvement: reduce code duplication for copy value --- .../SecretListView/SecretVersionItem.tsx | 61 ++++++++----------- 1 file changed, 25 insertions(+), 36 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretVersionItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretVersionItem.tsx index 2d3e123a0..1e24c83aa 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretVersionItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretVersionItem.tsx @@ -112,6 +112,29 @@ export const SecretVersionItem = ({ } }; + const handleCopyValue = async ( + e: React.MouseEvent | React.KeyboardEvent + ) => { + const value = await handleGetSecretValue(); + navigator.clipboard.writeText(value || ""); + const target = e.currentTarget; + target.style.borderBottom = "1px dashed"; + target.style.paddingBottom = "-1px"; + + // Create and insert popup + const popup = document.createElement("div"); + popup.className = + "w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50"; + popup.textContent = "Copied!"; + target.parentElement?.appendChild(popup); + + // Remove popup and border after delay + setTimeout(() => { + popup.remove(); + target.style.borderBottom = "none"; + }, 3000); + }; + return (
@@ -158,47 +181,13 @@ export const SecretVersionItem = ({ onClick={async (e) => { if (secretValueHidden) return; - const value = await handleGetSecretValue(); - navigator.clipboard.writeText(value || ""); - const target = e.currentTarget; - target.style.borderBottom = "1px dashed"; - target.style.paddingBottom = "-1px"; - - // Create and insert popup - const popup = document.createElement("div"); - popup.className = - "w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50"; - popup.textContent = "Copied!"; - target.parentElement?.appendChild(popup); - - // Remove popup and border after delay - setTimeout(() => { - popup.remove(); - target.style.borderBottom = "none"; - }, 3000); + await handleCopyValue(e); }} onKeyDown={async (e) => { if (secretValueHidden) return; if (e.key === "Enter" || e.key === " ") { - const value = await handleGetSecretValue(); - navigator.clipboard.writeText(value || ""); - const target = e.currentTarget; - target.style.borderBottom = "1px dashed"; - target.style.paddingBottom = "-1px"; - - // Create and insert popup - const popup = document.createElement("div"); - popup.className = - "w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50"; - popup.textContent = "Copied!"; - target.parentElement?.appendChild(popup); - - // Remove popup and border after delay - setTimeout(() => { - popup.remove(); - target.style.borderBottom = "none"; - }, 3000); + await handleCopyValue(e); } }} > From 55b240f32d8541f516cf1b68104651665028f70a Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 18 Sep 2025 12:37:45 -0700 Subject: [PATCH 08/10] improvement: skip edit/delete permissions for personal secrets --- .../secret-v2-bridge-service.ts | 60 ++++++++++--------- .../components/SecretListView/SecretItem.tsx | 3 +- 2 files changed, 33 insertions(+), 30 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 b3c8d3947..2a588f9a1 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 @@ -478,15 +478,16 @@ export const secretV2BridgeServiceFactory = ({ secret = sharedSecretToModify; } - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Edit, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: inputSecret.secretName, - secretTags: secret.tags.map((el) => el.slug) - }) - ); + if (secret.type !== SecretType.Personal) + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.Edit, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: inputSecret.secretName, + secretTags: secret.tags.map((el) => el.slug) + }) + ); // validate tags // fetch all tags and if not same count throw error meaning one was invalid tags @@ -497,17 +498,18 @@ export const secretV2BridgeServiceFactory = ({ const tagsToCheck = inputSecret.tagIds ? newTags : secret.tags; // now check with new ids - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Edit, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: inputSecret.secretName, - ...(tagsToCheck.length && { - secretTags: tagsToCheck.map((el) => el.slug) + if (secret.type !== SecretType.Personal) + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.Edit, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: inputSecret.secretName, + ...(tagsToCheck.length && { + secretTags: tagsToCheck.map((el) => el.slug) + }) }) - }) - ); + ); if (inputSecret.newSecretName) { const doesNewNameSecretExist = await secretDAL.findOne({ @@ -706,15 +708,17 @@ export const secretV2BridgeServiceFactory = ({ }) }); if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretActions.Delete, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: secretToDelete.key, - secretTags: secretToDelete.tags?.map((el) => el.slug) - }) - ); + + if (secretToDelete.type !== SecretType.Personal) + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.Delete, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: secretToDelete.key, + secretTags: secretToDelete.tags?.map((el) => el.slug) + }) + ); try { const deletedSecret = await secretDAL.transaction(async (tx) => { 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 e933d1b5e..e8894cb79 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx @@ -515,7 +515,6 @@ export const SecretItem = memo( isErrorLoadingValue={isErrorFetchingSecretValue} key="value-overriden" isVisible={isVisible} - isReadOnly={isReadOnly} {...field} onFocus={() => { if (secret.idOverride) setIsFieldFocused.on(); @@ -718,7 +717,7 @@ export const SecretItem = memo( Date: Thu, 18 Sep 2025 14:29:28 -0700 Subject: [PATCH 09/10] improvements: address more feedback --- frontend/src/hooks/api/dashboard/queries.tsx | 4 ++-- frontend/src/hooks/api/secrets/queries.tsx | 3 ++- .../SecretOverviewTableRow/SecretEditRow.tsx | 2 +- .../SecretDashboardPage.tsx | 24 +++++++++++++++++++ .../SecretMainPage.store.tsx | 1 + .../components/CommitForm/CommitForm.tsx | 17 ++++++++++--- .../components/SecretListView/SecretItem.tsx | 5 ++-- .../SecretListView/SecretListView.tsx | 10 ++++++-- 8 files changed, 55 insertions(+), 11 deletions(-) diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx index 0d2a0b8be..3866c6337 100644 --- a/frontend/src/hooks/api/dashboard/queries.tsx +++ b/frontend/src/hooks/api/dashboard/queries.tsx @@ -223,7 +223,7 @@ export const useGetProjectSecretsOverview = ( environments }); - queryClient.resetQueries({ + queryClient.invalidateQueries({ queryKey: dashboardKeys.getSecretValuesRoot() }); @@ -335,7 +335,7 @@ export const useGetProjectSecretsDetails = ( tags }); - queryClient.resetQueries({ + queryClient.invalidateQueries({ queryKey: dashboardKeys.getSecretValuesRoot() }); diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index 4eb2ba22f..95d0b9cdf 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -6,6 +6,7 @@ import axios from "axios"; import { createNotification } from "@app/components/notifications"; import { apiRequest } from "@app/config/request"; import { useToggle } from "@app/hooks/useToggle"; +import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem"; import { ERROR_NOT_ALLOWED_READ_SECRETS } from "./constants"; import { @@ -81,7 +82,7 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => { id: el.id, env: el.environment, key: el.secretKey, - value: el.secretValue, + value: el.secretValueHidden ? HIDDEN_SECRET_VALUE : el.secretValue, secretValueHidden: el.secretValueHidden, tags: el.tags || [], comment: el.secretComment || "", 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 9046f2ea7..a6ed5fc58 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -151,7 +151,7 @@ export const SecretEditRow = ({ formState: { isDirty, isSubmitting } } = useForm({ defaultValues: { - value: (secretValueData?.valueOverride ?? secretValueData?.value) || null + value: secretValueData?.valueOverride ?? secretValueData?.value ?? (defaultValue || null) } }); diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index 953353e5f..f6712e1fc 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -47,10 +47,12 @@ import { useGetWsTags } from "@app/hooks/api"; import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard"; +import { dashboardKeys } from "@app/hooks/api/dashboard/queries"; import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types"; import { useGetFolderCommitsCount } from "@app/hooks/api/folderCommits"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { ProjectVersion } from "@app/hooks/api/projects/types"; +import { queryClient } from "@app/hooks/api/reactQuery"; import { PendingAction } from "@app/hooks/api/secretFolders/types"; import { useCreateCommit } from "@app/hooks/api/secrets/mutations"; import { SecretV3RawSanitized } from "@app/hooks/api/types"; @@ -152,6 +154,10 @@ const Page = () => { } }, [isBatchMode, projectId, environment, secretPath, loadPendingChanges]); + useEffect(() => { + if (isVisible) setIsVisible(false); + }, [environment]); + const canReadSecret = hasSecretReadValueOrDescribePermission( permission, ProjectPermissionSecretActions.DescribeSecret, @@ -335,6 +341,24 @@ const Page = () => { pendingChanges: changes, message }); + + if (!isProtectedBranch) { + pendingChanges.secrets.forEach((secret) => { + if (secret.type === "update" && secret.secretValue !== undefined) { + queryClient.setQueryData( + dashboardKeys.getSecretValue({ + projectId, + environment, + secretPath, + secretKey: secret.newSecretName ?? secret.secretKey, + isOverride: false + }), + { value: secret.secretValue } + ); + } + }); + } + createNotification({ text: isProtectedBranch ? "Requested changes have been sent for review" diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx index 64367a7d1..06fecf169 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx @@ -54,6 +54,7 @@ export interface PendingSecretDelete extends BasePendingChange { type: PendingAction.Delete; secretKey: string; secretValue: string; + secretValueHidden: boolean; } // Folder-related change types diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx index 59cceb9bb..dbccd93e0 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CommitForm/CommitForm.tsx @@ -13,6 +13,7 @@ import { AnimatePresence, motion } from "framer-motion"; import { Badge, Button, Input, Modal, ModalContent } from "@app/components/v2"; import { PendingAction } from "@app/hooks/api/secretFolders/types"; import { SecretVersionDiffView } from "@app/pages/secret-manager/CommitDetailsPage/components/SecretVersionDiffView"; +import { HIDDEN_SECRET_VALUE_API_MASK } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem"; import { PendingChange, @@ -105,7 +106,12 @@ const RenderSecretChanges = ({ onDiscard, change }: RenderResourceProps) => { version: 1, // placeholder, not used secretKey: change.newSecretName ? existingSecret.key : undefined, secretValue: - change.secretValue !== undefined ? (change.originalValue ?? "") : undefined, + // eslint-disable-next-line no-nested-ternary + change.secretValue !== undefined + ? change.existingSecret.secretValueHidden + ? HIDDEN_SECRET_VALUE_API_MASK + : (change.originalValue ?? "") + : undefined, tags: change.tags ? (existingSecret.tags?.map((tag) => tag.slug) ?? []) : undefined, secretMetadata: change.secretMetadata ? existingSecret.secretMetadata : undefined, skipMultilineEncoding: @@ -130,7 +136,7 @@ const RenderSecretChanges = ({ onDiscard, change }: RenderResourceProps) => { } if (change.type === PendingAction.Delete) { - const { secretKey, secretValue } = change; + const { secretKey, secretValue, secretValueHidden } = change; return ( { { version: 1, // placeholder, not used secretKey, - secretValue: secretValue || undefined + // eslint-disable-next-line no-nested-ternary + secretValue: secretValue + ? secretValueHidden + ? HIDDEN_SECRET_VALUE_API_MASK + : secretValue + : undefined } ] }} 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 e8894cb79..6191e2a18 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx @@ -212,7 +212,8 @@ export const SecretItem = memo( reset, getValues, trigger, - formState: { isDirty, isSubmitting, errors } + formState: { isDirty, isSubmitting, errors }, + getFieldState } = useForm({ defaultValues: { ...secret, @@ -497,7 +498,7 @@ export const SecretItem = memo( tabIndex={0} role="button" > - {secretValueHidden && !isOverridden && !isPending && ( + {secretValueHidden && !getFieldState("value").isDirty && ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx index 010d6a4e2..4fc314bdd 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx @@ -466,7 +466,12 @@ export const SecretListView = ({ ); const handleSecretDelete = useCallback(async () => { - const { key, id: secretId, value } = popUp.deleteSecret?.data as SecretV3RawSanitized; + const { + key, + id: secretId, + value, + secretValueHidden + } = popUp.deleteSecret?.data as SecretV3RawSanitized; try { if (isBatchMode) { const deleteChange: PendingSecretDelete = { @@ -475,7 +480,8 @@ export const SecretListView = ({ secretKey: key, secretValue: value || "", timestamp: Date.now(), - resourceType: "secret" + resourceType: "secret", + secretValueHidden }; addPendingChange(deleteChange, { From 37018581ef6366785991fc035e52666646fa83b2 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 18 Sep 2025 17:29:57 -0700 Subject: [PATCH 10/10] improvements: final feedback --- .../components/SecretListView/SecretDetailSidebar.tsx | 2 ++ .../components/SecretListView/SecretVersionItem.tsx | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) 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 c0ba7184e..67404f878 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -403,6 +403,7 @@ export const SecretDetailSidebar = ({ isLoadingSecretValue || isErrorFetchingSecretValue } + canEditButNotView={secret.secretValueHidden && canEditSecretValue} environment={environment} secretPath={secretPath} key="secret-value" @@ -703,6 +704,7 @@ export const SecretDetailSidebar = ({
{secretVersion?.map((version) => ( void; + canReadValue: boolean; } export const SecretVersionItem = ({ secretVersion: { createdAt, version, actor, secretValueHidden }, secret, currentVersion, - onRevert + onRevert, + canReadValue }: SecretVersionItemProps) => { const { currentProject } = useProject(); @@ -244,7 +246,7 @@ export const SecretVersionItem = ({
- {!secret?.isRotatedSecret && ( + {!secret?.isRotatedSecret && canReadValue && (