mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-10 07:58:15 -05:00
wip
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<ReturnType<typeof server.services.secretImport.getImportsMultiEnv>> | undefined;
|
||||
let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined;
|
||||
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>> | undefined;
|
||||
let secrets:
|
||||
| (Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>>[number] & { isEmpty: boolean })[]
|
||||
| undefined;
|
||||
let dynamicSecrets:
|
||||
| Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnvs>>
|
||||
| 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<ReturnType<typeof server.services.folder.getFolders>> | undefined;
|
||||
let secrets:
|
||||
| (Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"][number] & {
|
||||
isEmpty: boolean;
|
||||
reminder: Awaited<ReturnType<typeof server.services.reminder.getRemindersForDashboard>>[string] | null;
|
||||
})[]
|
||||
| undefined;
|
||||
let dynamicSecrets: Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnv>> | undefined;
|
||||
let secretRotations:
|
||||
| Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>
|
||||
| (Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>[number] & {
|
||||
secrets: (NonNullable<
|
||||
Awaited<
|
||||
ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>
|
||||
>[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 };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -159,6 +159,7 @@ export type TGetSecretVersionsDTO = Omit<TProjectPermission, "projectId"> & {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
secretId: string;
|
||||
secretVersions?: string[];
|
||||
};
|
||||
|
||||
export type TSecretReference = { environment: string; secretPath: string; secretKey: string };
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -331,6 +331,7 @@ export type TGetSecretVersionsDTO = Omit<TProjectPermission, "projectId"> & {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
secretId: string;
|
||||
secretVersions?: string[];
|
||||
};
|
||||
|
||||
export type TSecretReference = { environment: string; secretPath: string };
|
||||
|
||||
@@ -279,11 +279,16 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
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}
|
||||
|
||||
@@ -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 } = {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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<ReturnType<typeof fetchProjectSecretsOverview>>) => {
|
||||
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<ReturnType<typeof fetchProjectSecretsDetails>>) => ({
|
||||
...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<DashboardSecretValue>("/api/v1/dashboard/secret-value", {
|
||||
params
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetSecretValue = (
|
||||
params: TGetSecretValueDTO,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
DashboardSecretValue,
|
||||
unknown,
|
||||
DashboardSecretValue,
|
||||
ReturnType<typeof dashboardKeys.getSecretValue>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.getSecretValue(params),
|
||||
queryFn: async () => fetchSecretValue(params),
|
||||
staleTime: 1000 * 60,
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
@@ -112,7 +112,6 @@ export type TGetDashboardProjectSecretsDetailsDTO = Omit<
|
||||
TGetDashboardProjectSecretsOverviewDTO,
|
||||
"environments"
|
||||
> & {
|
||||
viewSecretValue: boolean;
|
||||
environment: string;
|
||||
includeImports?: boolean;
|
||||
tags: Record<string, boolean>;
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -28,7 +28,7 @@ export type TImportedSecrets = {
|
||||
environmentInfo: ProjectEnv;
|
||||
secretPath: string;
|
||||
folderId: string;
|
||||
secrets: SecretV3Raw[];
|
||||
secrets: Omit<SecretV3Raw, "secretValue">[];
|
||||
};
|
||||
|
||||
export type TGetSecretImports = {
|
||||
|
||||
@@ -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<string, { id: string; value?: string; env: string }> = {};
|
||||
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<TSecretVersionValue>(
|
||||
`/api/v1/dashboard/secret-versions/${secretId}/value/${version}`,
|
||||
{
|
||||
params: {
|
||||
secretId,
|
||||
version
|
||||
}
|
||||
}
|
||||
);
|
||||
return data.value;
|
||||
};
|
||||
|
||||
export const useGetSecretVersionValue = (
|
||||
dto: TGetSecretVersionValue,
|
||||
options?: Omit<
|
||||
UseQueryOptions<string, unknown, string, ReturnType<typeof secretKeys.getSecretVersionValue>>,
|
||||
"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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 (
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={secret ? undefined : "You do not have permission to view this secret."}
|
||||
>
|
||||
<tr className="!last:border-b-0 h-full hover:bg-mineshaft-700">
|
||||
<td className="flex h-full items-center" style={{ padding: "0.5rem 1rem" }}>
|
||||
<span className={twMerge(!secret && "blur")}>{secret?.key ?? "********"}</span>
|
||||
</td>
|
||||
<td className="col-span-2 h-full w-full" style={{ padding: "0.5rem 1rem" }}>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{!secret ? (
|
||||
<div className="h-full pl-4 blur">********</div>
|
||||
) : secret.secretValueHidden ? (
|
||||
<Blur
|
||||
className="py-0"
|
||||
tooltipText="You do not have permission to read the value of this secret."
|
||||
/>
|
||||
) : (
|
||||
<SecretInput
|
||||
isReadOnly
|
||||
value={secretValue}
|
||||
isVisible={isSecretVisible}
|
||||
onFocus={() => setIsFieldFocused.on()}
|
||||
onBlur={() => setIsFieldFocused.off()}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -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 = ({
|
||||
<tbody className="border-t-2 border-mineshaft-600">
|
||||
{secrets.map((secret, index) => {
|
||||
return (
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={
|
||||
secret
|
||||
? undefined
|
||||
: "You do not have permission to view this secret."
|
||||
}
|
||||
>
|
||||
<tr
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`rotation-secret-${secretRotation.id}-${index}`}
|
||||
className="!last:border-b-0 h-full hover:bg-mineshaft-700"
|
||||
>
|
||||
<td
|
||||
className="flex h-full items-center"
|
||||
style={{ padding: "0.5rem 1rem" }}
|
||||
>
|
||||
<span className={twMerge(!secret && "blur")}>
|
||||
{secret?.key ?? "********"}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className="col-span-2 h-full w-full"
|
||||
style={{ padding: "0.5rem 1rem" }}
|
||||
>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{!secret ? (
|
||||
<div className="h-full pl-4 blur">********</div>
|
||||
) : secret.secretValueHidden ? (
|
||||
<Blur
|
||||
className="py-0"
|
||||
tooltipText="You do not have permission to read the value of this secret."
|
||||
/>
|
||||
) : (
|
||||
<InfisicalSecretInput
|
||||
isReadOnly
|
||||
value={secret.value}
|
||||
isVisible={isSecretVisible}
|
||||
secretPath={secretRotation.folder.path}
|
||||
environment={secretRotation.environment.slug}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</Tooltip>
|
||||
<SecretOverviewRotationSecretRow
|
||||
key={`rotation-secret-${secretRotation.id}-${index + 1}`}
|
||||
secret={secret}
|
||||
secretPath={secretRotation.folder.path}
|
||||
environment={secretRotation.environment.slug}
|
||||
isSecretVisible={isSecretVisible}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
|
||||
@@ -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<void>;
|
||||
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
|
||||
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 = ({
|
||||
)}
|
||||
<div className="flex-grow border-r border-r-mineshaft-600 pl-1 pr-2">
|
||||
<Controller
|
||||
disabled={isImportedSecret && !defaultValue}
|
||||
control={control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<InfisicalSecretInput
|
||||
{...field}
|
||||
isReadOnly={isImportedSecret || (isRotatedSecret && !isOverride)}
|
||||
value={field.value as string}
|
||||
isReadOnly={
|
||||
isImportedSecret ||
|
||||
(isRotatedSecret && !isOverride) ||
|
||||
isFetchingSecretValue ||
|
||||
isErrorFetchingSecretValue
|
||||
}
|
||||
value={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
isFetchingSecretValue
|
||||
? HIDDEN_SECRET_VALUE
|
||||
: isErrorFetchingSecretValue
|
||||
? "Error fetching secret value..."
|
||||
: (field.value as string)
|
||||
}
|
||||
key="secret-input"
|
||||
isVisible={isVisible && !secretValueHidden}
|
||||
secretPath={secretPath}
|
||||
@@ -236,6 +324,11 @@ export const SecretEditRow = ({
|
||||
isImport={isImportedSecret}
|
||||
defaultValue={secretValueHidden ? "" : undefined}
|
||||
canEditButNotView={secretValueHidden && !isOverride}
|
||||
onFocus={() => setIsFieldFocused.on()}
|
||||
onBlur={() => {
|
||||
field.onBlur();
|
||||
setIsFieldFocused.off();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<Td
|
||||
key={`sec-overview-${slug}-${i + 1}-value`}
|
||||
@@ -254,7 +261,7 @@ export const SecretOverviewTableRow = ({
|
||||
<FontAwesomeIcon icon={faRotate} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{secret?.valueOverride && (
|
||||
{secret?.idOverride && (
|
||||
<Tooltip content="Personal Override">
|
||||
<FontAwesomeIcon icon={faCodeBranch} />
|
||||
</Tooltip>
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
</Tooltip>
|
||||
</Tab>
|
||||
)}
|
||||
{currentProject.environments.length > 1 && (
|
||||
{/* scott: removing until we have time to update for fetching secret value */}
|
||||
{/* {currentProject.environments.length > 1 && (
|
||||
<Tab className="ml-auto" value={COMPARE_ENVIRONMENT_TAB}>
|
||||
<div className="flex items-center gap-x-2 whitespace-nowrap">
|
||||
<FontAwesomeIcon icon={faArrowRightArrowLeft} />
|
||||
Compare Environments
|
||||
</div>
|
||||
</Tab>
|
||||
)}
|
||||
)} */}
|
||||
</TabList>
|
||||
</Tabs>
|
||||
<Modal
|
||||
|
||||
@@ -20,12 +20,14 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2";
|
||||
import { EmptyState, IconButton, TableContainer, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useResyncSecretReplication } from "@app/hooks/api";
|
||||
import { TSecretImport } from "@app/hooks/api/types";
|
||||
|
||||
import { SecretImportSecretRow } from "./SecretImportSecretRow";
|
||||
|
||||
type Props = {
|
||||
onDelete: () => 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 = ({
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{filteredImportedSecrets.map(({ key, value }, index) => (
|
||||
<tr key={`${id}-${key}-${index + 1}`}>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
{key}
|
||||
</td>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<SecretInput value={value} isReadOnly />
|
||||
</td>
|
||||
{/* <td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
|
||||
</td> */}
|
||||
</tr>
|
||||
{filteredImportedSecrets.map((secret, index) => (
|
||||
<SecretImportSecretRow
|
||||
secret={secret}
|
||||
key={`${id}-${secret.key}-${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<tr>
|
||||
<td className="h-10 w-1/2" style={{ padding: "0.25rem 1rem" }}>
|
||||
{key}
|
||||
</td>
|
||||
<td className="h-10 w-1/2" style={{ padding: "0.25rem 1rem" }}>
|
||||
<SecretInput
|
||||
value={getValue()}
|
||||
onFocus={() => setIsFieldFocused.on()}
|
||||
onBlur={() => setIsFieldFocused.off()}
|
||||
isReadOnly
|
||||
/>
|
||||
</td>
|
||||
{/* <td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
|
||||
</td> */}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
@@ -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<TFormSchema>({
|
||||
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()}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSubmit(handleFormSubmit)}
|
||||
@@ -356,7 +399,13 @@ export const SecretDetailSidebar = ({
|
||||
>
|
||||
<div className="flex items-start gap-x-2">
|
||||
<InfisicalSecretInput
|
||||
isReadOnly={isReadOnly || !isAllowed || secret?.isRotatedSecret}
|
||||
isReadOnly={
|
||||
isReadOnly ||
|
||||
!isAllowed ||
|
||||
secret?.isRotatedSecret ||
|
||||
isLoadingSecretValue ||
|
||||
isErrorFetchingSecretValue
|
||||
}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
key="secret-value"
|
||||
@@ -364,6 +413,11 @@ export const SecretDetailSidebar = ({
|
||||
containerClassName="text-bunker-300 w-full hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
{...field}
|
||||
autoFocus={false}
|
||||
onFocus={() => setIsFieldFocused.on()}
|
||||
onBlur={() => {
|
||||
setIsFieldFocused.off();
|
||||
field.onBlur();
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
content={
|
||||
@@ -378,11 +432,17 @@ export const SecretDetailSidebar = ({
|
||||
className="px-2 py-[0.43rem] font-normal"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faShare} />}
|
||||
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 = ({
|
||||
<div className="dark flex max-h-[24rem] flex-1 cursor-default flex-col text-sm text-bunker-300">
|
||||
<div className="mb-0.5 text-mineshaft-400">Version History</div>
|
||||
<div className="thin-scrollbar flex flex-1 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
|
||||
{secretVersion?.map(
|
||||
({ createdAt, secretValue, secretValueHidden, version, id, actor }) => (
|
||||
<div className="flex flex-row" key={id}>
|
||||
<div className="flex w-full flex-col space-y-1">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10">
|
||||
<div className="w-fit rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 text-sm text-mineshaft-300">
|
||||
v{version}
|
||||
</div>
|
||||
</div>
|
||||
<div>{format(new Date(createdAt), "Pp")}</div>
|
||||
</div>
|
||||
<div className="flex w-full cursor-default">
|
||||
<div className="relative w-10">
|
||||
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
|
||||
</div>
|
||||
<div className="flex w-full cursor-default flex-col">
|
||||
{actor && (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex w-fit flex-row text-sm">
|
||||
Modified by:
|
||||
<Tooltip content={getModifiedByName(actor.actorType, actor.name)}>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
onClick={() =>
|
||||
onModifyHistoryClick(
|
||||
actor.actorId,
|
||||
actor.actorType,
|
||||
actor.membershipId
|
||||
)
|
||||
}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={getModifiedByIcon(actor.actorType)}
|
||||
className="ml-2"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row">
|
||||
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
|
||||
Value:
|
||||
</div>
|
||||
<div className="group break-all pl-1 font-mono">
|
||||
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
|
||||
<button
|
||||
type="button"
|
||||
className="select-none text-left"
|
||||
onClick={(e) => {
|
||||
if (secretValueHidden) return;
|
||||
{secretVersion?.map((version) => (
|
||||
<SecretVersionItem
|
||||
secretVersion={version}
|
||||
secret={secret}
|
||||
currentVersion={secretVersion.length}
|
||||
onRevert={async (versionValue) => {
|
||||
await fetchValue();
|
||||
|
||||
navigator.clipboard.writeText(secretValue || "");
|
||||
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);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (secretValueHidden) return;
|
||||
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
navigator.clipboard.writeText(secretValue || "");
|
||||
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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={twMerge(
|
||||
secretValueHidden && "text-xs text-bunker-300 opacity-40"
|
||||
)}
|
||||
>
|
||||
{secretValueHidden ? "Hidden" : secretValue}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.currentTarget
|
||||
.closest(".group")
|
||||
?.classList.remove("show-value");
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.stopPropagation();
|
||||
e.currentTarget
|
||||
.closest(".group")
|
||||
?.classList.remove("show-value");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEyeSlash} />
|
||||
</button>
|
||||
</div>
|
||||
<span className="group-[.show-value]:hidden">
|
||||
{secretValueHidden
|
||||
? HIDDEN_SECRET_VALUE
|
||||
: secretValue?.replace(/./g, "*")}
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.currentTarget
|
||||
.closest(".group")
|
||||
?.classList.add("show-value");
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.currentTarget
|
||||
.closest(".group")
|
||||
?.classList.add("show-value");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!secret?.isRotatedSecret && (
|
||||
<div
|
||||
className={`flex items-center justify-center ${version === secretVersion.length ? "hidden" : ""}`}
|
||||
>
|
||||
<Tooltip content="Restore Secret Value">
|
||||
<IconButton
|
||||
ariaLabel="Restore"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
className="h-8 w-8 rounded-md"
|
||||
onClick={() => setValue("value", secretValue, { shouldDirty: true })}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRotateRight} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
setTimeout(() => {
|
||||
setValue("value", versionValue, { shouldDirty: true });
|
||||
}, 5);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="dark flex flex-col text-sm text-bunker-300">
|
||||
|
||||
@@ -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 = "<hidden-by-infisical>";
|
||||
|
||||
type Props = {
|
||||
secret: SecretV3RawSanitized;
|
||||
secret: SecretV3RawSanitized & { originalKey?: string };
|
||||
onSaveSecret: (
|
||||
orgSec: SecretV3RawSanitized,
|
||||
modSec: Omit<SecretV3RawSanitized, "tags"> & { 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<NodeJS.Timeout>();
|
||||
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<TFormSchema>({
|
||||
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<TFormSchema>({
|
||||
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
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Tooltip content="Share Secret">
|
||||
<FontAwesomeSymbol
|
||||
|
||||
@@ -616,19 +616,21 @@ export const SecretListView = ({
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<SecretDetailSidebar
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
isOpen={popUp.secretDetail.isOpen}
|
||||
onToggle={(isOpen) => 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 && (
|
||||
<SecretDetailSidebar
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
isOpen={popUp.secretDetail.isOpen}
|
||||
onToggle={(isOpen) => 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 })}
|
||||
/>
|
||||
)}
|
||||
<CreateTagModal
|
||||
isOpen={popUp.createTag.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("createTag", isOpen)}
|
||||
|
||||
@@ -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<string | null>(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 (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex w-full flex-col space-y-1">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10">
|
||||
<div className="w-fit rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 text-sm text-mineshaft-300">
|
||||
v{version}
|
||||
</div>
|
||||
</div>
|
||||
<div>{format(new Date(createdAt), "Pp")}</div>
|
||||
</div>
|
||||
<div className="flex w-full cursor-default">
|
||||
<div className="relative w-10">
|
||||
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
|
||||
</div>
|
||||
<div className="flex w-full cursor-default flex-col">
|
||||
{actor && (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex w-fit flex-row text-sm">
|
||||
Modified by:
|
||||
<Tooltip content={getModifiedByName(actor.actorType, actor.name)}>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
onClick={() =>
|
||||
onModifyHistoryClick(actor.actorId, actor.actorType, actor.membershipId)
|
||||
}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<FontAwesomeIcon icon={getModifiedByIcon(actor.actorType)} className="ml-2" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row">
|
||||
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
|
||||
Value:
|
||||
</div>
|
||||
<div className="group break-all pl-1 font-mono">
|
||||
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
|
||||
<button
|
||||
type="button"
|
||||
className="select-none text-left"
|
||||
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);
|
||||
}}
|
||||
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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={twMerge(secretValueHidden && "text-xs text-bunker-300 opacity-40")}
|
||||
>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{secretValueHidden
|
||||
? "Hidden"
|
||||
: isFetchingValue
|
||||
? "****"
|
||||
: ((secretValue || <span className="text-mineshaft-400">EMPTY</span>) ??
|
||||
"Error fetching secret value...")}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.currentTarget.closest(".group")?.classList.remove("show-value");
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.stopPropagation();
|
||||
e.currentTarget.closest(".group")?.classList.remove("show-value");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEyeSlash} />
|
||||
</button>
|
||||
</div>
|
||||
<span className="group-[.show-value]:hidden">
|
||||
****
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer"
|
||||
onClick={async (e) => {
|
||||
e.currentTarget.closest(".group")?.classList.add("show-value");
|
||||
await handleGetSecretValue();
|
||||
}}
|
||||
onKeyDown={async (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.currentTarget.closest(".group")?.classList.add("show-value");
|
||||
await handleGetSecretValue();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!secret?.isRotatedSecret && (
|
||||
<div
|
||||
className={`flex items-center justify-center ${version === currentVersion ? "hidden" : ""}`}
|
||||
>
|
||||
<Tooltip content="Restore Secret Value">
|
||||
<IconButton
|
||||
ariaLabel="Restore"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
className="h-8 w-8 rounded-md"
|
||||
onClick={async () => {
|
||||
if (secretValue) {
|
||||
onRevert(secretValue);
|
||||
return;
|
||||
}
|
||||
|
||||
const value = await handleGetSecretValue();
|
||||
|
||||
onRevert(value);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRotateRight} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 = ({
|
||||
<tbody>
|
||||
{secrets.map((secret, index) => {
|
||||
return (
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={
|
||||
secret ? undefined : "You do not have permission to view this secret."
|
||||
}
|
||||
>
|
||||
<tr
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`rotation-secret-${secretRotation.id}-${index}`}
|
||||
className="h-full last:!border-b-0 hover:bg-mineshaft-700"
|
||||
>
|
||||
<td className="flex h-full items-center" style={{ padding: "0.5rem 1rem" }}>
|
||||
<span className={twMerge(!secret && "blur")}>
|
||||
{secret?.key ?? "********"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="col-span-2 h-full w-full" style={{ padding: "0.5rem 1rem" }}>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{!secret ? (
|
||||
<div className="h-full pl-4 blur">********</div>
|
||||
) : secret.secretValueHidden ? (
|
||||
<Blur
|
||||
className="py-0"
|
||||
tooltipText="You do not have permission to read the value of this secret."
|
||||
/>
|
||||
) : (
|
||||
<InfisicalSecretInput
|
||||
isReadOnly
|
||||
value={secret.value}
|
||||
secretPath={secretRotation.folder.path}
|
||||
environment={secretRotation.environment.slug}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</Tooltip>
|
||||
<SecretRotationSecretRow
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`rotation-secret-${secretRotation.id}-${index}`}
|
||||
secret={secret}
|
||||
environment={secretRotation.environment.slug}
|
||||
secretPath={secretRotation.folder.path}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
|
||||
@@ -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 (
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={secret ? undefined : "You do not have permission to view this secret."}
|
||||
>
|
||||
<tr className="h-full last:!border-b-0 hover:bg-mineshaft-700">
|
||||
<td className="flex h-full items-center" style={{ padding: "0.5rem 1rem" }}>
|
||||
<span className={twMerge(!secret && "blur")}>{secret?.key ?? "********"}</span>
|
||||
</td>
|
||||
<td className="col-span-2 h-full w-full" style={{ padding: "0.5rem 1rem" }}>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{!secret ? (
|
||||
<div className="h-full pl-4 blur">********</div>
|
||||
) : secret.secretValueHidden ? (
|
||||
<Blur
|
||||
className="py-0"
|
||||
tooltipText="You do not have permission to read the value of this secret."
|
||||
/>
|
||||
) : (
|
||||
<SecretInput
|
||||
isReadOnly
|
||||
value={getValue()}
|
||||
onFocus={() => setIsFieldFocused.on()}
|
||||
onBlur={() => setIsFieldFocused.off()}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user