mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 23:26:09 +00:00
Merge pull request #3191 from Infisical/feat/addActorToVersionHistory
Add actor to secret version history
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretVersionV2)) {
|
||||||
|
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
|
||||||
|
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
|
||||||
|
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
|
||||||
|
if (!hasSecretVersionV2UserActorId) {
|
||||||
|
t.uuid("userActorId");
|
||||||
|
t.foreign("userActorId").references("id").inTable(TableName.Users);
|
||||||
|
}
|
||||||
|
if (!hasSecretVersionV2IdentityActorId) {
|
||||||
|
t.uuid("identityActorId");
|
||||||
|
t.foreign("identityActorId").references("id").inTable(TableName.Identity);
|
||||||
|
}
|
||||||
|
if (!hasSecretVersionV2ActorType) {
|
||||||
|
t.string("actorType");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretVersionV2)) {
|
||||||
|
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
|
||||||
|
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
|
||||||
|
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
|
||||||
|
if (hasSecretVersionV2UserActorId) {
|
||||||
|
t.dropColumn("userActorId");
|
||||||
|
}
|
||||||
|
if (hasSecretVersionV2IdentityActorId) {
|
||||||
|
t.dropColumn("identityActorId");
|
||||||
|
}
|
||||||
|
if (hasSecretVersionV2ActorType) {
|
||||||
|
t.dropColumn("actorType");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,10 @@ export const SecretVersionsV2Schema = z.object({
|
|||||||
folderId: z.string().uuid(),
|
folderId: z.string().uuid(),
|
||||||
userId: z.string().uuid().nullable().optional(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
userActorId: z.string().uuid().nullable().optional(),
|
||||||
|
identityActorId: z.string().uuid().nullable().optional(),
|
||||||
|
actorType: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;
|
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { NotFoundError } from "@app/lib/errors";
|
|||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
@@ -332,6 +333,7 @@ export const secretRotationQueueFactory = ({
|
|||||||
await secretVersionV2BridgeDAL.insertMany(
|
await secretVersionV2BridgeDAL.insertMany(
|
||||||
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||||
...el,
|
...el,
|
||||||
|
actorType: ActorType.PLATFORM,
|
||||||
secretId: id
|
secretId: id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
|||||||
import { InternalServerError, NotFoundError } from "@app/lib/errors";
|
import { InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
@@ -370,7 +371,21 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
const secrets = await secretV2BridgeDAL.insertMany(
|
const secrets = await secretV2BridgeDAL.insertMany(
|
||||||
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
|
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
|
||||||
secretVersions.map(
|
secretVersions.map(
|
||||||
({ latestSecretVersion, version, updatedAt, createdAt, secretId, envId, id, tags, ...el }) => ({
|
({
|
||||||
|
latestSecretVersion,
|
||||||
|
version,
|
||||||
|
updatedAt,
|
||||||
|
createdAt,
|
||||||
|
secretId,
|
||||||
|
envId,
|
||||||
|
id,
|
||||||
|
tags,
|
||||||
|
// exclude the bottom fields from the secret - they are for versioning only.
|
||||||
|
userActorId,
|
||||||
|
identityActorId,
|
||||||
|
actorType,
|
||||||
|
...el
|
||||||
|
}) => ({
|
||||||
...el,
|
...el,
|
||||||
id: secretId,
|
id: secretId,
|
||||||
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
|
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
|
||||||
@@ -401,8 +416,18 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
const userActorId = actor === ActorType.USER ? actorId : undefined;
|
||||||
|
const identityActorId = actor !== ActorType.USER ? actorId : undefined;
|
||||||
|
const actorType = actor || ActorType.PLATFORM;
|
||||||
|
|
||||||
const secretVersions = await secretVersionV2BridgeDAL.insertMany(
|
const secretVersions = await secretVersionV2BridgeDAL.insertMany(
|
||||||
secrets.map(({ id, updatedAt, createdAt, ...el }) => ({ ...el, secretId: id })),
|
secrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||||
|
...el,
|
||||||
|
secretId: id,
|
||||||
|
userActorId,
|
||||||
|
identityActorId,
|
||||||
|
actorType
|
||||||
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
await secretVersionV2TagBridgeDAL.insertMany(
|
await secretVersionV2TagBridgeDAL.insertMany(
|
||||||
|
|||||||
@@ -111,7 +111,16 @@ export const secretRawSchema = z.object({
|
|||||||
secretReminderRepeatDays: z.number().nullable().optional(),
|
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||||
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
actor: z
|
||||||
|
.object({
|
||||||
|
actorId: z.string().nullable().optional(),
|
||||||
|
actorType: z.string().nullable().optional(),
|
||||||
|
name: z.string().nullable().optional(),
|
||||||
|
membershipId: z.string().nullable().optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ProjectPermissionSchema = z.object({
|
export const ProjectPermissionSchema = z.object({
|
||||||
|
|||||||
@@ -772,6 +772,10 @@ export const importDataIntoInfisicalFn = async ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
|||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
@@ -62,6 +63,7 @@ export const fnSecretBulkInsert = async ({
|
|||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor,
|
||||||
tx
|
tx
|
||||||
}: TFnSecretBulkInsert) => {
|
}: TFnSecretBulkInsert) => {
|
||||||
const sanitizedInputSecrets = inputSecrets.map(
|
const sanitizedInputSecrets = inputSecrets.map(
|
||||||
@@ -90,6 +92,10 @@ export const fnSecretBulkInsert = async ({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const userActorId = actor && actor.type === ActorType.USER ? actor.actorId : undefined;
|
||||||
|
const identityActorId = actor && actor.type !== ActorType.USER ? actor.actorId : undefined;
|
||||||
|
const actorType = actor?.type || ActorType.PLATFORM;
|
||||||
|
|
||||||
const newSecrets = await secretDAL.insertMany(
|
const newSecrets = await secretDAL.insertMany(
|
||||||
sanitizedInputSecrets.map((el) => ({ ...el, folderId })),
|
sanitizedInputSecrets.map((el) => ({ ...el, folderId })),
|
||||||
tx
|
tx
|
||||||
@@ -106,6 +112,9 @@ export const fnSecretBulkInsert = async ({
|
|||||||
sanitizedInputSecrets.map((el) => ({
|
sanitizedInputSecrets.map((el) => ({
|
||||||
...el,
|
...el,
|
||||||
folderId,
|
folderId,
|
||||||
|
userActorId,
|
||||||
|
identityActorId,
|
||||||
|
actorType,
|
||||||
secretId: newSecretGroupedByKeyName[el.key][0].id
|
secretId: newSecretGroupedByKeyName[el.key][0].id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
@@ -157,8 +166,13 @@ export const fnSecretBulkUpdate = async ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
actor
|
||||||
}: TFnSecretBulkUpdate) => {
|
}: TFnSecretBulkUpdate) => {
|
||||||
|
const userActorId = actor && actor?.type === ActorType.USER ? actor?.actorId : undefined;
|
||||||
|
const identityActorId = actor && actor?.type !== ActorType.USER ? actor?.actorId : undefined;
|
||||||
|
const actorType = actor?.type || ActorType.PLATFORM;
|
||||||
|
|
||||||
const sanitizedInputSecrets = inputSecrets.map(
|
const sanitizedInputSecrets = inputSecrets.map(
|
||||||
({
|
({
|
||||||
filter,
|
filter,
|
||||||
@@ -216,7 +230,10 @@ export const fnSecretBulkUpdate = async ({
|
|||||||
encryptedValue,
|
encryptedValue,
|
||||||
reminderRepeatDays,
|
reminderRepeatDays,
|
||||||
folderId,
|
folderId,
|
||||||
secretId
|
secretId,
|
||||||
|
userActorId,
|
||||||
|
identityActorId,
|
||||||
|
actorType
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
tx
|
tx
|
||||||
@@ -616,6 +633,12 @@ export const reshapeBridgeSecret = (
|
|||||||
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
|
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
|
||||||
value: string;
|
value: string;
|
||||||
comment: string;
|
comment: string;
|
||||||
|
userActorName?: string | null;
|
||||||
|
identityActorName?: string | null;
|
||||||
|
userActorId?: string | null;
|
||||||
|
identityActorId?: string | null;
|
||||||
|
membershipId?: string | null;
|
||||||
|
actorType?: string | null;
|
||||||
tags?: {
|
tags?: {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -636,6 +659,14 @@ export const reshapeBridgeSecret = (
|
|||||||
_id: secret.id,
|
_id: secret.id,
|
||||||
id: secret.id,
|
id: secret.id,
|
||||||
user: secret.userId,
|
user: secret.userId,
|
||||||
|
actor: secret.actorType
|
||||||
|
? {
|
||||||
|
actorType: secret.actorType,
|
||||||
|
actorId: secret.userActorId || secret.identityActorId,
|
||||||
|
name: secret.identityActorName || secret.userActorName,
|
||||||
|
membershipId: secret.membershipId
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
tags: secret.tags,
|
tags: secret.tags,
|
||||||
skipMultilineEncoding: secret.skipMultilineEncoding,
|
skipMultilineEncoding: secret.skipMultilineEncoding,
|
||||||
secretReminderRepeatDays: secret.reminderRepeatDays,
|
secretReminderRepeatDays: secret.reminderRepeatDays,
|
||||||
|
|||||||
@@ -301,6 +301,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -483,6 +487,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -1230,6 +1238,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -1490,6 +1502,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL
|
||||||
});
|
});
|
||||||
updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
|
updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
|
||||||
@@ -1522,6 +1538,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
});
|
});
|
||||||
updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
|
updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
|
||||||
@@ -1689,14 +1709,19 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
projectId: folder.projectId
|
projectId: folder.projectId
|
||||||
});
|
});
|
||||||
const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] });
|
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors(secretId, folder.projectId, {
|
||||||
return secretVersions.map((el) =>
|
offset,
|
||||||
reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
|
limit,
|
||||||
|
sort: [["createdAt", "desc"]]
|
||||||
|
});
|
||||||
|
|
||||||
|
return secretVersions.map((el) => {
|
||||||
|
return reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
|
||||||
...el,
|
...el,
|
||||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||||
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// this is a backfilling API for secret references
|
// this is a backfilling API for secret references
|
||||||
@@ -1956,6 +1981,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
||||||
return {
|
return {
|
||||||
type: doc.type,
|
type: doc.type,
|
||||||
@@ -1982,6 +2011,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
tx,
|
tx,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
inputSecrets: locallyUpdatedSecrets.map((doc) => {
|
inputSecrets: locallyUpdatedSecrets.map((doc) => {
|
||||||
return {
|
return {
|
||||||
filter: {
|
filter: {
|
||||||
|
|||||||
@@ -168,6 +168,10 @@ export type TFnSecretBulkInsert = {
|
|||||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">;
|
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
|
actor?: {
|
||||||
|
type: string;
|
||||||
|
actorId: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type TRequireReferenceIfValue =
|
type TRequireReferenceIfValue =
|
||||||
@@ -192,6 +196,10 @@ export type TFnSecretBulkUpdate = {
|
|||||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">;
|
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
|
actor?: {
|
||||||
|
type: string;
|
||||||
|
actorId: string;
|
||||||
|
};
|
||||||
tx?: Knex;
|
tx?: Knex;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
|
import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
|
||||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueName } from "@app/queue";
|
import { QueueName } from "@app/queue";
|
||||||
|
|
||||||
@@ -119,11 +120,67 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`);
|
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findVersionsBySecretIdWithActors = async (
|
||||||
|
secretId: string,
|
||||||
|
projectId: string,
|
||||||
|
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TSecretVersionsV2> = {},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const query = (tx || db)(TableName.SecretVersionV2)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectMembership,
|
||||||
|
`${TableName.ProjectMembership}.userId`,
|
||||||
|
`${TableName.SecretVersionV2}.userActorId`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
|
||||||
|
.where((qb) => {
|
||||||
|
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
||||||
|
void qb.where(`${TableName.ProjectMembership}.projectId`, projectId);
|
||||||
|
})
|
||||||
|
.orWhere((qb) => {
|
||||||
|
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
||||||
|
void qb.whereNull(`${TableName.ProjectMembership}.projectId`);
|
||||||
|
})
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.SecretVersionV2),
|
||||||
|
`${TableName.Users}.username as userActorName`,
|
||||||
|
`${TableName.Identity}.name as identityActorName`,
|
||||||
|
`${TableName.ProjectMembership}.id as membershipId`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (limit) void query.limit(limit);
|
||||||
|
if (offset) void query.offset(offset);
|
||||||
|
if (sort) {
|
||||||
|
void query.orderBy(
|
||||||
|
sort.map(([column, order, nulls]) => ({
|
||||||
|
column: `${TableName.SecretVersionV2}.${column as string}`,
|
||||||
|
order,
|
||||||
|
nulls
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs: Array<
|
||||||
|
TSecretVersionsV2 & {
|
||||||
|
userActorName: string | undefined | null;
|
||||||
|
identityActorName: string | undefined | null;
|
||||||
|
membershipId: string | undefined | null;
|
||||||
|
}
|
||||||
|
> = await query;
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindVersionsBySecretIdWithActors" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretVersionV2Orm,
|
...secretVersionV2Orm,
|
||||||
pruneExcessVersions,
|
pruneExcessVersions,
|
||||||
findLatestVersionMany,
|
findLatestVersionMany,
|
||||||
bulkUpdate,
|
bulkUpdate,
|
||||||
findLatestVersionByFolderId
|
findLatestVersionByFolderId,
|
||||||
|
findVersionsBySecretIdWithActors
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -579,6 +579,7 @@ export const fnSecretBulkInsert = async ({
|
|||||||
[`${TableName.Secret}Id` as const]: newSecretGroupByBlindIndex[secretBlindIndex as string][0].id
|
[`${TableName.Secret}Id` as const]: newSecretGroupByBlindIndex[secretBlindIndex as string][0].id
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
const secretVersions = await secretVersionDAL.insertMany(
|
const secretVersions = await secretVersionDAL.insertMany(
|
||||||
sanitizedInputSecrets.map((el) => ({
|
sanitizedInputSecrets.map((el) => ({
|
||||||
...el,
|
...el,
|
||||||
|
|||||||
@@ -101,6 +101,12 @@ export type SecretVersions = {
|
|||||||
skipMultilineEncoding?: boolean;
|
skipMultilineEncoding?: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
actor?: {
|
||||||
|
actorId?: string | null;
|
||||||
|
actorType?: string | null;
|
||||||
|
name?: string | null;
|
||||||
|
membershipId?: string | null;
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// dto
|
// dto
|
||||||
|
|||||||
+167
-75
@@ -5,15 +5,18 @@ import {
|
|||||||
faArrowRotateRight,
|
faArrowRotateRight,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
faClock,
|
faClock,
|
||||||
|
faDesktop,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faPlus,
|
faPlus,
|
||||||
|
faServer,
|
||||||
faShare,
|
faShare,
|
||||||
faTag,
|
faTag,
|
||||||
faTrash
|
faTrash,
|
||||||
|
faUser
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link, useNavigate } from "@tanstack/react-router";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
@@ -46,6 +49,7 @@ import {
|
|||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import { useGetSecretVersion } from "@app/hooks/api";
|
import { useGetSecretVersion } from "@app/hooks/api";
|
||||||
|
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||||
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
|
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
|
||||||
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
@@ -120,6 +124,7 @@ export const SecretDetailSidebar = ({
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const selectTagSlugs = selectedTags.map((i) => i.slug);
|
const selectTagSlugs = selectedTags.map((i) => i.slug);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const cannotEditSecret = permission.cannot(
|
const cannotEditSecret = permission.cannot(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionActions.Edit,
|
||||||
@@ -192,15 +197,73 @@ export const SecretDetailSidebar = ({
|
|||||||
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReminderSubmit = async (reminderRepeatDays: number | null | undefined, reminderNote: string | null | undefined) => {
|
const handleReminderSubmit = async (
|
||||||
await onSaveSecret(secret, { ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true }, () => { });
|
reminderRepeatDays: number | null | undefined,
|
||||||
}
|
reminderNote: string | null | undefined
|
||||||
|
) => {
|
||||||
|
await onSaveSecret(
|
||||||
|
secret,
|
||||||
|
{ ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true },
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
|
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
|
||||||
|
|
||||||
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
||||||
const secretReminderNote = watch("reminderNote");
|
const secretReminderNote = watch("reminderNote");
|
||||||
|
|
||||||
|
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";
|
||||||
|
default:
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLinkToModifyHistoryEntity = (
|
||||||
|
actorId: string,
|
||||||
|
actorType: string,
|
||||||
|
membershipId: string | null = ""
|
||||||
|
) => {
|
||||||
|
switch (actorType) {
|
||||||
|
case ActorType.USER:
|
||||||
|
return `/${ProjectType.SecretManager}/${currentWorkspace.id}/members/${membershipId}`;
|
||||||
|
case ActorType.IDENTITY:
|
||||||
|
return `/${ProjectType.SecretManager}/${currentWorkspace.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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateReminderForm
|
<CreateReminderForm
|
||||||
@@ -213,7 +276,7 @@ export const SecretDetailSidebar = ({
|
|||||||
if (data) {
|
if (data) {
|
||||||
setValue("reminderRepeatDays", data.days, { shouldDirty: false });
|
setValue("reminderRepeatDays", data.days, { shouldDirty: false });
|
||||||
setValue("reminderNote", data.note, { shouldDirty: false });
|
setValue("reminderNote", data.note, { shouldDirty: false });
|
||||||
handleReminderSubmit(data.days, data.note)
|
handleReminderSubmit(data.days, data.note);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -618,7 +681,7 @@ export const SecretDetailSidebar = ({
|
|||||||
<div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300">
|
<div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300">
|
||||||
<div className="mb-2 pl-1">Version History</div>
|
<div className="mb-2 pl-1">Version History</div>
|
||||||
<div className="thin-scrollbar flex h-48 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]">
|
<div className="thin-scrollbar flex h-48 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, version, id }) => (
|
{secretVersion?.map(({ createdAt, secretValue, version, id, actor }) => (
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<div key={id} className="flex w-full flex-col space-y-1">
|
<div key={id} className="flex w-full flex-col space-y-1">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -633,36 +696,42 @@ export const SecretDetailSidebar = ({
|
|||||||
<div className="relative w-10">
|
<div className="relative w-10">
|
||||||
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
|
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row">
|
<div className="flex w-full cursor-default flex-col">
|
||||||
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
|
{actor && (
|
||||||
Value:
|
<div className="flex flex-row">
|
||||||
</div>
|
<div className="flex w-fit flex-row text-sm">
|
||||||
<div className="group break-all pl-1 font-mono">
|
Modified by:
|
||||||
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
|
<Tooltip content={getModifiedByName(actor.actorType, actor.name)}>
|
||||||
<button
|
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||||
type="button"
|
<div
|
||||||
className="select-none"
|
onClick={() =>
|
||||||
onClick={(e) => {
|
onModifyHistoryClick(
|
||||||
navigator.clipboard.writeText(secretValue || "");
|
actor.actorId,
|
||||||
const target = e.currentTarget;
|
actor.actorType,
|
||||||
target.style.borderBottom = "1px dashed";
|
actor.membershipId
|
||||||
target.style.paddingBottom = "-1px";
|
)
|
||||||
|
}
|
||||||
// Create and insert popup
|
className="cursor-pointer"
|
||||||
const popup = document.createElement("div");
|
>
|
||||||
popup.className =
|
<FontAwesomeIcon
|
||||||
"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";
|
icon={getModifiedByIcon(actor.actorType)}
|
||||||
popup.textContent = "Copied!";
|
className="ml-2"
|
||||||
target.parentElement?.appendChild(popup);
|
/>
|
||||||
|
</div>
|
||||||
// Remove popup and border after delay
|
</Tooltip>
|
||||||
setTimeout(() => {
|
</div>
|
||||||
popup.remove();
|
</div>
|
||||||
target.style.borderBottom = "none";
|
)}
|
||||||
}, 3000);
|
<div className="flex flex-row">
|
||||||
}}
|
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
|
||||||
onKeyDown={(e) => {
|
Value:
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
</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) => {
|
||||||
navigator.clipboard.writeText(secretValue || "");
|
navigator.clipboard.writeText(secretValue || "");
|
||||||
const target = e.currentTarget;
|
const target = e.currentTarget;
|
||||||
target.style.borderBottom = "1px dashed";
|
target.style.borderBottom = "1px dashed";
|
||||||
@@ -680,51 +749,74 @@ export const SecretDetailSidebar = ({
|
|||||||
popup.remove();
|
popup.remove();
|
||||||
target.style.borderBottom = "none";
|
target.style.borderBottom = "none";
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}}
|
||||||
}}
|
onKeyDown={(e) => {
|
||||||
>
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
{secretValue}
|
navigator.clipboard.writeText(secretValue || "");
|
||||||
</button>
|
const target = e.currentTarget;
|
||||||
<button
|
target.style.borderBottom = "1px dashed";
|
||||||
type="button"
|
target.style.paddingBottom = "-1px";
|
||||||
className="ml-1 cursor-pointer"
|
|
||||||
onClick={(e) => {
|
// Create and insert popup
|
||||||
e.stopPropagation();
|
const popup = document.createElement("div");
|
||||||
e.currentTarget
|
popup.className =
|
||||||
.closest(".group")
|
"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";
|
||||||
?.classList.remove("show-value");
|
popup.textContent = "Copied!";
|
||||||
}}
|
target.parentElement?.appendChild(popup);
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
// Remove popup and border after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
popup.remove();
|
||||||
|
target.style.borderBottom = "none";
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{secretValue}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-1 cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.currentTarget
|
e.currentTarget
|
||||||
.closest(".group")
|
.closest(".group")
|
||||||
?.classList.remove("show-value");
|
?.classList.remove("show-value");
|
||||||
}
|
}}
|
||||||
}}
|
onKeyDown={(e) => {
|
||||||
>
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
<FontAwesomeIcon icon={faEyeSlash} />
|
e.stopPropagation();
|
||||||
</button>
|
e.currentTarget
|
||||||
</div>
|
.closest(".group")
|
||||||
<span className="group-[.show-value]:hidden">
|
?.classList.remove("show-value");
|
||||||
{secretValue?.replace(/./g, "*")}
|
}
|
||||||
<button
|
}}
|
||||||
type="button"
|
>
|
||||||
className="ml-1 cursor-pointer"
|
<FontAwesomeIcon icon={faEyeSlash} />
|
||||||
onClick={(e) => {
|
</button>
|
||||||
e.currentTarget.closest(".group")?.classList.add("show-value");
|
</div>
|
||||||
}}
|
<span className="group-[.show-value]:hidden">
|
||||||
onKeyDown={(e) => {
|
{secretValue?.replace(/./g, "*")}
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-1 cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
e.currentTarget
|
e.currentTarget
|
||||||
.closest(".group")
|
.closest(".group")
|
||||||
?.classList.add("show-value");
|
?.classList.add("show-value");
|
||||||
}
|
}}
|
||||||
}}
|
onKeyDown={(e) => {
|
||||||
>
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
<FontAwesomeIcon icon={faEye} />
|
e.currentTarget
|
||||||
</button>
|
.closest(".group")
|
||||||
</span>
|
?.classList.add("show-value");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEye} />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+3
-1
@@ -241,7 +241,9 @@ export const SecretListView = ({
|
|||||||
|
|
||||||
let successMessage;
|
let successMessage;
|
||||||
if (isReminderEvent) {
|
if (isReminderEvent) {
|
||||||
successMessage = reminderRepeatDays ? "Successfully saved secret reminder" : "Successfully deleted secret reminder";
|
successMessage = reminderRepeatDays
|
||||||
|
? "Successfully saved secret reminder"
|
||||||
|
: "Successfully deleted secret reminder";
|
||||||
} else {
|
} else {
|
||||||
successMessage = "Successfully saved secrets";
|
successMessage = "Successfully saved secrets";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user