Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Knex } from "knex";

import { TableName } from "../schemas";

export async function up(knex: Knex): Promise<void> {
const hasIsRedactedColumn = await knex.schema.hasColumn(TableName.SecretVersionV2, "isRedacted");
const hasRedactedAtColumn = await knex.schema.hasColumn(TableName.SecretVersionV2, "redactedAt");
const hasRedactedByUserColumn = await knex.schema.hasColumn(TableName.SecretVersionV2, "redactedByUserId");
const hasParentVersionIdColumn = await knex.schema.hasColumn(TableName.SecretVersionV2, "parentVersionId");

const missingColumns =
!hasIsRedactedColumn || !hasRedactedAtColumn || !hasRedactedByUserColumn || !hasParentVersionIdColumn;

if (missingColumns) {
await knex.schema.alterTable(TableName.SecretVersionV2, (table) => {
if (!hasParentVersionIdColumn) {
table.uuid("parentVersionId").references("id").inTable(TableName.SecretVersionV2).onDelete("SET NULL");
table.index("parentVersionId");
}

if (!hasIsRedactedColumn) table.boolean("isRedacted").defaultTo(false).notNullable();
if (!hasRedactedAtColumn) table.timestamp("redactedAt").nullable();
if (!hasRedactedByUserColumn)
table.uuid("redactedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
});
}
}

export async function down(knex: Knex): Promise<void> {
const hasIsRedactedColumn = await knex.schema.hasColumn(TableName.SecretVersionV2, "isRedacted");
const hasRedactedAtColumn = await knex.schema.hasColumn(TableName.SecretVersionV2, "redactedAt");
const hasRedactedByUserColumn = await knex.schema.hasColumn(TableName.SecretVersionV2, "redactedByUserId");
const hasParentVersionIdColumn = await knex.schema.hasColumn(TableName.SecretVersionV2, "parentVersionId");
const hasColumns = hasIsRedactedColumn || hasRedactedAtColumn || hasRedactedByUserColumn || hasParentVersionIdColumn;

if (hasColumns) {
await knex.schema.alterTable(TableName.SecretVersionV2, (table) => {
if (hasParentVersionIdColumn) {
table.dropIndex("parentVersionId");
table.dropColumn("parentVersionId");
}
if (hasIsRedactedColumn) table.dropColumn("isRedacted");
if (hasRedactedAtColumn) table.dropColumn("redactedAt");
if (hasRedactedByUserColumn) table.dropColumn("redactedByUserId");
});
}
}
6 changes: 5 additions & 1 deletion backend/src/db/schemas/secret-versions-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ export const SecretVersionsV2Schema = z.object({
updatedAt: z.date(),
userActorId: z.string().uuid().nullable().optional(),
identityActorId: z.string().uuid().nullable().optional(),
actorType: z.string().nullable().optional()
actorType: z.string().nullable().optional(),
parentVersionId: z.string().uuid().nullable().optional(),
isRedacted: z.boolean().default(false),
redactedAt: z.date().nullable().optional(),
redactedByUserId: z.string().uuid().nullable().optional()
});

export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;
Expand Down
3 changes: 3 additions & 0 deletions backend/src/ee/routes/v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { registerDeprecatedProjectRoleRouter } from "./deprecated-project-role-r
import { registerGatewayV2Router } from "./gateway-router";
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
import { registerSecretVersionRouter } from "./secret-version-router";

export const registerV2EERoutes = async (server: FastifyZodProvider) => {
await server.register(
Expand Down Expand Up @@ -54,4 +55,6 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => {
},
{ prefix: "/secret-scanning" }
);

await server.register(registerSecretVersionRouter, { prefix: "/secret-versions" });
};
56 changes: 56 additions & 0 deletions backend/src/ee/routes/v2/secret-version-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { z } from "zod";

import { SecretVersionsV2Schema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";

export const registerSecretVersionRouter = async (server: FastifyZodProvider) => {
server.route({
method: "DELETE",
url: "/:versionId/redact-value",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
versionId: z.string()
}),
response: {
200: z.object({
secretVersion: SecretVersionsV2Schema.omit({ encryptedValue: true, encryptedComment: true })
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secretVersion, projectId, environment, secretPath, secretKey, secretId } =
await server.services.secret.redactSecretVersionValue({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
versionId: req.params.versionId
});

await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.REDACT_SECRET_VERSION_VALUE,
metadata: {
environment,
secretPath,
secretId,
secretKey,
secretVersionId: secretVersion.id,
secretVersion: secretVersion.version
}
}
});

return { secretVersion };
}
});
};
14 changes: 14 additions & 0 deletions backend/src/ee/services/audit-log/audit-log-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export enum EventType {
MOVE_SECRETS = "move-secrets",
DELETE_SECRET = "delete-secret",
DELETE_SECRETS = "delete-secrets",
REDACT_SECRET_VERSION_VALUE = "redact-secret-version-value",
GET_PROJECT_KEY = "get-project-key",
AUTHORIZE_INTEGRATION = "authorize-integration",
UPDATE_INTEGRATION_AUTH = "update-integration-auth",
Expand Down Expand Up @@ -896,6 +897,18 @@ interface DeleteSecretBatchEvent {
};
}

interface RedactSecretVersionValueEvent {
type: EventType.REDACT_SECRET_VERSION_VALUE;
metadata: {
environment: string;
secretPath: string;
secretId: string;
secretKey: string;
secretVersionId: string;
secretVersion: number;
};
}

interface GetProjectKeyEvent {
type: EventType.GET_PROJECT_KEY;
metadata: {
Expand Down Expand Up @@ -5066,6 +5079,7 @@ export type Event =
| MoveSecretsEvent
| DeleteSecretEvent
| DeleteSecretBatchEvent
| RedactSecretVersionValueEvent
| GetProjectKeyEvent
| AuthorizeIntegrationEvent
| UpdateIntegrationAuthEvent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,19 @@ export const secretReplicationServiceFactory = ({
const sourceSecrets = $getReplicatedSecretsV2(sourceDecryptedLocalSecrets, sourceImportedSecrets);
const sourceSecretsGroupByKey = groupBy(sourceSecrets, (i) => i.key);

// Fetch latest version IDs for all source secrets to track parent-child relationships
const sourceSecretsGroupedByFolderId = groupBy(sourceSecrets, (s) => s.folderId);
const sourceSecretLatestVersions: Record<string, string> = {};
await Promise.all(
Object.entries(sourceSecretsGroupedByFolderId).map(async ([folderId, secrets]) => {
const secretIds = secrets.map((s) => s.id);
const latestVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, secretIds);
Object.entries(latestVersions).forEach(([secretId, version]) => {
sourceSecretLatestVersions[secretId] = version.id;
});
})
);

const lock = await keyStore.acquireLock(
[getReplicationKeyLockPrefix(projectId, environmentSlug, secretPath)],
5000
Expand Down Expand Up @@ -495,7 +508,8 @@ export const secretReplicationServiceFactory = ({
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
secretMetadata: doc.rawSecretMetadata,
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : [],
parentSecretVersionId: sourceSecretLatestVersions[doc.id]
};
})
});
Expand Down Expand Up @@ -525,7 +539,8 @@ export const secretReplicationServiceFactory = ({
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
secretMetadata: doc.rawSecretMetadata,
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : [],
parentSecretVersionId: sourceSecretLatestVersions[doc.id]
}
};
})
Expand Down
3 changes: 2 additions & 1 deletion backend/src/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1637,7 +1637,8 @@ export const registerRoutes = async (
secretV2BridgeService,
secretApprovalRequestService,
licenseService,
reminderService
reminderService,
secretVersionV2DAL: secretVersionV2BridgeDAL
});

const secretSharingService = secretSharingServiceFactory({
Expand Down
11 changes: 11 additions & 0 deletions backend/src/server/routes/v1/dashboard-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1698,6 +1698,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secretVersions: secretRawSchema
.omit({ secretValue: true })
.extend({
isRedacted: z.boolean(),
redactedByActor: z
.object({
username: z.string().nullable(),
email: z.string().nullable().optional(),
projectMembershipId: z.string().uuid().nullable().optional()
})
.nullable()
.optional(),
redactedAt: z.date().nullable(),
redactedByUserId: z.string().uuid().nullable(),
secretValueHidden: z.boolean()
})
.array()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export type SecretCommitChange = BaseCommitChangeInfo & {
tags?: string[] | null;
secretReminderRecipients?: string[] | null;
secretValue: string;
isRedacted: boolean;
redactedAt: Date | null;
redactedByUserId: string | null;
}[];
};

Expand Down
10 changes: 8 additions & 2 deletions backend/src/services/folder-commit/folder-commit-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ const secretVersionSchema = z.object({
skipMultilineEncoding: z.boolean().nullable().optional(),
tags: z.array(z.string()).nullable().optional(),
metadata: z.unknown().nullable().optional(),
secretValue: z.string()
secretValue: z.string(),
isRedacted: z.boolean(),
redactedAt: z.date().nullable(),
redactedByUserId: z.string().nullable()
});

// Folder-specific versions schema
Expand Down Expand Up @@ -122,7 +125,10 @@ const secretResourceChangeSchema = baseResourceChangeSchema.extend({
tags: z.array(z.string()).nullable().optional(),
metadata: z.unknown().nullable().optional(),
secretReminderNote: z.string().nullable().optional(),
secretValue: z.string().optional()
secretValue: z.string().optional(),
isRedacted: z.boolean(),
redactedAt: z.date().nullable(),
redactedByUserId: z.string().nullable()
})
)
.optional()
Expand Down
3 changes: 3 additions & 0 deletions backend/src/services/folder-commit/folder-commit-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ type SecretChange = BaseChange & {
metadata?: unknown;
tags?: string[] | null;
secretValue?: string;
isRedacted: boolean;
redactedAt: Date | null;
redactedByUserId: string | null;
}[];
};

Expand Down
Loading
Loading