Skip to content

Commit 5651e61

Browse files
authored
Merge pull request #5392 from Infisical/daniel/secret-version-value-redaction
feat: secret version value redaction
2 parents 48646e6 + 3b5f860 commit 5651e61

File tree

29 files changed

+1011
-291
lines changed

29 files changed

+1011
-291
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Knex } from "knex";
2+
3+
import { TableName } from "../schemas";
4+
5+
export async function up(knex: Knex): Promise<void> {
6+
const hasIsRedactedColumn = await knex.schema.hasColumn(TableName.SecretVersionV2, "isRedacted");
7+
const hasRedactedAtColumn = await knex.schema.hasColumn(TableName.SecretVersionV2, "redactedAt");
8+
const hasRedactedByUserColumn = await knex.schema.hasColumn(TableName.SecretVersionV2, "redactedByUserId");
9+
const hasParentVersionIdColumn = await knex.schema.hasColumn(TableName.SecretVersionV2, "parentVersionId");
10+
11+
const missingColumns =
12+
!hasIsRedactedColumn || !hasRedactedAtColumn || !hasRedactedByUserColumn || !hasParentVersionIdColumn;
13+
14+
if (missingColumns) {
15+
await knex.schema.alterTable(TableName.SecretVersionV2, (table) => {
16+
if (!hasParentVersionIdColumn) {
17+
table.uuid("parentVersionId").references("id").inTable(TableName.SecretVersionV2).onDelete("SET NULL");
18+
table.index("parentVersionId");
19+
}
20+
21+
if (!hasIsRedactedColumn) table.boolean("isRedacted").defaultTo(false).notNullable();
22+
if (!hasRedactedAtColumn) table.timestamp("redactedAt").nullable();
23+
if (!hasRedactedByUserColumn)
24+
table.uuid("redactedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
25+
});
26+
}
27+
}
28+
29+
export async function down(knex: Knex): Promise<void> {
30+
const hasIsRedactedColumn = await knex.schema.hasColumn(TableName.SecretVersionV2, "isRedacted");
31+
const hasRedactedAtColumn = await knex.schema.hasColumn(TableName.SecretVersionV2, "redactedAt");
32+
const hasRedactedByUserColumn = await knex.schema.hasColumn(TableName.SecretVersionV2, "redactedByUserId");
33+
const hasParentVersionIdColumn = await knex.schema.hasColumn(TableName.SecretVersionV2, "parentVersionId");
34+
const hasColumns = hasIsRedactedColumn || hasRedactedAtColumn || hasRedactedByUserColumn || hasParentVersionIdColumn;
35+
36+
if (hasColumns) {
37+
await knex.schema.alterTable(TableName.SecretVersionV2, (table) => {
38+
if (hasParentVersionIdColumn) {
39+
table.dropIndex("parentVersionId");
40+
table.dropColumn("parentVersionId");
41+
}
42+
if (hasIsRedactedColumn) table.dropColumn("isRedacted");
43+
if (hasRedactedAtColumn) table.dropColumn("redactedAt");
44+
if (hasRedactedByUserColumn) table.dropColumn("redactedByUserId");
45+
});
46+
}
47+
}

backend/src/db/schemas/secret-versions-v2.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ export const SecretVersionsV2Schema = z.object({
2828
updatedAt: z.date(),
2929
userActorId: z.string().uuid().nullable().optional(),
3030
identityActorId: z.string().uuid().nullable().optional(),
31-
actorType: z.string().nullable().optional()
31+
actorType: z.string().nullable().optional(),
32+
parentVersionId: z.string().uuid().nullable().optional(),
33+
isRedacted: z.boolean().default(false),
34+
redactedAt: z.date().nullable().optional(),
35+
redactedByUserId: z.string().uuid().nullable().optional()
3236
});
3337

3438
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;

backend/src/ee/routes/v2/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { registerDeprecatedProjectRoleRouter } from "./deprecated-project-role-r
1111
import { registerGatewayV2Router } from "./gateway-router";
1212
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
1313
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
14+
import { registerSecretVersionRouter } from "./secret-version-router";
1415

1516
export const registerV2EERoutes = async (server: FastifyZodProvider) => {
1617
await server.register(
@@ -54,4 +55,6 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => {
5455
},
5556
{ prefix: "/secret-scanning" }
5657
);
58+
59+
await server.register(registerSecretVersionRouter, { prefix: "/secret-versions" });
5760
};
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { z } from "zod";
2+
3+
import { SecretVersionsV2Schema } from "@app/db/schemas";
4+
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
5+
import { writeLimit } from "@app/server/config/rateLimiter";
6+
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
7+
import { AuthMode } from "@app/services/auth/auth-type";
8+
9+
export const registerSecretVersionRouter = async (server: FastifyZodProvider) => {
10+
server.route({
11+
method: "DELETE",
12+
url: "/:versionId/redact-value",
13+
config: {
14+
rateLimit: writeLimit
15+
},
16+
schema: {
17+
params: z.object({
18+
versionId: z.string()
19+
}),
20+
response: {
21+
200: z.object({
22+
secretVersion: SecretVersionsV2Schema.omit({ encryptedValue: true, encryptedComment: true })
23+
})
24+
}
25+
},
26+
onRequest: verifyAuth([AuthMode.JWT]),
27+
handler: async (req) => {
28+
const { secretVersion, projectId, environment, secretPath, secretKey, secretId } =
29+
await server.services.secret.redactSecretVersionValue({
30+
actor: req.permission.type,
31+
actorId: req.permission.id,
32+
actorAuthMethod: req.permission.authMethod,
33+
actorOrgId: req.permission.orgId,
34+
versionId: req.params.versionId
35+
});
36+
37+
await server.services.auditLog.createAuditLog({
38+
projectId,
39+
...req.auditLogInfo,
40+
event: {
41+
type: EventType.REDACT_SECRET_VERSION_VALUE,
42+
metadata: {
43+
environment,
44+
secretPath,
45+
secretId,
46+
secretKey,
47+
secretVersionId: secretVersion.id,
48+
secretVersion: secretVersion.version
49+
}
50+
}
51+
});
52+
53+
return { secretVersion };
54+
}
55+
});
56+
};

backend/src/ee/services/audit-log/audit-log-types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export enum EventType {
151151
MOVE_SECRETS = "move-secrets",
152152
DELETE_SECRET = "delete-secret",
153153
DELETE_SECRETS = "delete-secrets",
154+
REDACT_SECRET_VERSION_VALUE = "redact-secret-version-value",
154155
GET_PROJECT_KEY = "get-project-key",
155156
AUTHORIZE_INTEGRATION = "authorize-integration",
156157
UPDATE_INTEGRATION_AUTH = "update-integration-auth",
@@ -896,6 +897,18 @@ interface DeleteSecretBatchEvent {
896897
};
897898
}
898899

900+
interface RedactSecretVersionValueEvent {
901+
type: EventType.REDACT_SECRET_VERSION_VALUE;
902+
metadata: {
903+
environment: string;
904+
secretPath: string;
905+
secretId: string;
906+
secretKey: string;
907+
secretVersionId: string;
908+
secretVersion: number;
909+
};
910+
}
911+
899912
interface GetProjectKeyEvent {
900913
type: EventType.GET_PROJECT_KEY;
901914
metadata: {
@@ -5066,6 +5079,7 @@ export type Event =
50665079
| MoveSecretsEvent
50675080
| DeleteSecretEvent
50685081
| DeleteSecretBatchEvent
5082+
| RedactSecretVersionValueEvent
50695083
| GetProjectKeyEvent
50705084
| AuthorizeIntegrationEvent
50715085
| UpdateIntegrationAuthEvent

backend/src/ee/services/secret-replication/secret-replication-service.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,19 @@ export const secretReplicationServiceFactory = ({
309309
const sourceSecrets = $getReplicatedSecretsV2(sourceDecryptedLocalSecrets, sourceImportedSecrets);
310310
const sourceSecretsGroupByKey = groupBy(sourceSecrets, (i) => i.key);
311311

312+
// Fetch latest version IDs for all source secrets to track parent-child relationships
313+
const sourceSecretsGroupedByFolderId = groupBy(sourceSecrets, (s) => s.folderId);
314+
const sourceSecretLatestVersions: Record<string, string> = {};
315+
await Promise.all(
316+
Object.entries(sourceSecretsGroupedByFolderId).map(async ([folderId, secrets]) => {
317+
const secretIds = secrets.map((s) => s.id);
318+
const latestVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, secretIds);
319+
Object.entries(latestVersions).forEach(([secretId, version]) => {
320+
sourceSecretLatestVersions[secretId] = version.id;
321+
});
322+
})
323+
);
324+
312325
const lock = await keyStore.acquireLock(
313326
[getReplicationKeyLockPrefix(projectId, environmentSlug, secretPath)],
314327
5000
@@ -495,7 +508,8 @@ export const secretReplicationServiceFactory = ({
495508
encryptedComment: doc.encryptedComment,
496509
skipMultilineEncoding: doc.skipMultilineEncoding,
497510
secretMetadata: doc.rawSecretMetadata,
498-
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
511+
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : [],
512+
parentSecretVersionId: sourceSecretLatestVersions[doc.id]
499513
};
500514
})
501515
});
@@ -525,7 +539,8 @@ export const secretReplicationServiceFactory = ({
525539
encryptedComment: doc.encryptedComment,
526540
skipMultilineEncoding: doc.skipMultilineEncoding,
527541
secretMetadata: doc.rawSecretMetadata,
528-
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
542+
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : [],
543+
parentSecretVersionId: sourceSecretLatestVersions[doc.id]
529544
}
530545
};
531546
})

backend/src/server/routes/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1637,7 +1637,8 @@ export const registerRoutes = async (
16371637
secretV2BridgeService,
16381638
secretApprovalRequestService,
16391639
licenseService,
1640-
reminderService
1640+
reminderService,
1641+
secretVersionV2DAL: secretVersionV2BridgeDAL
16411642
});
16421643

16431644
const secretSharingService = secretSharingServiceFactory({

backend/src/server/routes/v1/dashboard-router.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,6 +1698,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
16981698
secretVersions: secretRawSchema
16991699
.omit({ secretValue: true })
17001700
.extend({
1701+
isRedacted: z.boolean(),
1702+
redactedByActor: z
1703+
.object({
1704+
username: z.string().nullable(),
1705+
email: z.string().nullable().optional(),
1706+
projectMembershipId: z.string().uuid().nullable().optional()
1707+
})
1708+
.nullable()
1709+
.optional(),
1710+
redactedAt: z.date().nullable(),
1711+
redactedByUserId: z.string().uuid().nullable(),
17011712
secretValueHidden: z.boolean()
17021713
})
17031714
.array()

backend/src/services/folder-commit-changes/folder-commit-changes-dal.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ export type SecretCommitChange = BaseCommitChangeInfo & {
4242
tags?: string[] | null;
4343
secretReminderRecipients?: string[] | null;
4444
secretValue: string;
45+
isRedacted: boolean;
46+
redactedAt: Date | null;
47+
redactedByUserId: string | null;
4548
}[];
4649
};
4750

backend/src/services/folder-commit/folder-commit-schemas.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ const secretVersionSchema = z.object({
2929
skipMultilineEncoding: z.boolean().nullable().optional(),
3030
tags: z.array(z.string()).nullable().optional(),
3131
metadata: z.unknown().nullable().optional(),
32-
secretValue: z.string()
32+
secretValue: z.string(),
33+
isRedacted: z.boolean(),
34+
redactedAt: z.date().nullable(),
35+
redactedByUserId: z.string().nullable()
3336
});
3437

3538
// Folder-specific versions schema
@@ -122,7 +125,10 @@ const secretResourceChangeSchema = baseResourceChangeSchema.extend({
122125
tags: z.array(z.string()).nullable().optional(),
123126
metadata: z.unknown().nullable().optional(),
124127
secretReminderNote: z.string().nullable().optional(),
125-
secretValue: z.string().optional()
128+
secretValue: z.string().optional(),
129+
isRedacted: z.boolean(),
130+
redactedAt: z.date().nullable(),
131+
redactedByUserId: z.string().nullable()
126132
})
127133
)
128134
.optional()

0 commit comments

Comments
 (0)