mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 10:35:17 +00:00
Merge remote-tracking branch 'origin/main' into misc/privilege-management-v2-transition
This commit is contained in:
@@ -772,6 +772,10 @@ export const importDataIntoInfisicalFn = async ({
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
},
|
||||
tx
|
||||
});
|
||||
}
|
||||
|
||||
@@ -114,20 +114,27 @@ export const integrationAuthServiceFactory = ({
|
||||
const listOrgIntegrationAuth = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGenericPermission) => {
|
||||
const authorizations = await integrationAuthDAL.getByOrg(actorOrgId as string);
|
||||
|
||||
return Promise.all(
|
||||
authorizations.filter(async (auth) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: auth.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
const filteredAuthorizations = await Promise.all(
|
||||
authorizations.map(async (auth) => {
|
||||
try {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: auth.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
|
||||
return permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
return permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations) ? auth : null;
|
||||
} catch (error) {
|
||||
// user does not belong to the project that the integration auth belongs to
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return filteredAuthorizations.filter((auth): auth is NonNullable<typeof auth> => auth !== null);
|
||||
};
|
||||
|
||||
const getIntegrationAuth = async ({ actor, id, actorId, actorAuthMethod, actorOrgId }: TGetIntegrationAuthDTO) => {
|
||||
|
||||
@@ -613,6 +613,9 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
|
||||
.leftJoin(TableName.SecretFolder, `${TableName.SecretV2}.folderId`, `${TableName.SecretFolder}.id`)
|
||||
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||
.select(selectAllTableCols(TableName.SecretV2))
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
@@ -622,12 +625,13 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||
);
|
||||
)
|
||||
.select(db.ref("projectId").withSchema(TableName.Environment).as("projectId"));
|
||||
|
||||
const docs = sqlNestRelationships({
|
||||
data: rawDocs,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }),
|
||||
parentMapper: (el) => ({ _id: el.id, projectId: el.projectId, ...SecretsV2Schema.parse(el) }),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "tagId",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
@@ -62,6 +63,7 @@ export const fnSecretBulkInsert = async ({
|
||||
resourceMetadataDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
actor,
|
||||
tx
|
||||
}: TFnSecretBulkInsert) => {
|
||||
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(
|
||||
sanitizedInputSecrets.map((el) => ({ ...el, folderId })),
|
||||
tx
|
||||
@@ -106,6 +112,9 @@ export const fnSecretBulkInsert = async ({
|
||||
sanitizedInputSecrets.map((el) => ({
|
||||
...el,
|
||||
folderId,
|
||||
userActorId,
|
||||
identityActorId,
|
||||
actorType,
|
||||
secretId: newSecretGroupedByKeyName[el.key][0].id
|
||||
})),
|
||||
tx
|
||||
@@ -157,8 +166,13 @@ export const fnSecretBulkUpdate = async ({
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
resourceMetadataDAL
|
||||
resourceMetadataDAL,
|
||||
actor
|
||||
}: 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(
|
||||
({
|
||||
filter,
|
||||
@@ -216,7 +230,10 @@ export const fnSecretBulkUpdate = async ({
|
||||
encryptedValue,
|
||||
reminderRepeatDays,
|
||||
folderId,
|
||||
secretId
|
||||
secretId,
|
||||
userActorId,
|
||||
identityActorId,
|
||||
actorType
|
||||
})
|
||||
),
|
||||
tx
|
||||
@@ -616,6 +633,12 @@ export const reshapeBridgeSecret = (
|
||||
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
|
||||
value: string;
|
||||
comment: string;
|
||||
userActorName?: string | null;
|
||||
identityActorName?: string | null;
|
||||
userActorId?: string | null;
|
||||
identityActorId?: string | null;
|
||||
membershipId?: string | null;
|
||||
actorType?: string | null;
|
||||
tags?: {
|
||||
id: string;
|
||||
slug: string;
|
||||
@@ -636,6 +659,14 @@ export const reshapeBridgeSecret = (
|
||||
_id: secret.id,
|
||||
id: secret.id,
|
||||
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,
|
||||
skipMultilineEncoding: secret.skipMultilineEncoding,
|
||||
secretReminderRepeatDays: secret.reminderRepeatDays,
|
||||
|
||||
@@ -28,6 +28,7 @@ import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
||||
import { TSecretQueueFactory } from "../secret/secret-queue";
|
||||
import { TGetASecretByIdDTO } from "../secret/secret-types";
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
||||
@@ -73,7 +74,13 @@ type TSecretV2BridgeServiceFactoryDep = {
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
|
||||
folderDAL: Pick<
|
||||
TSecretFolderDALFactory,
|
||||
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findBySecretPathMultiEnv"
|
||||
| "findBySecretPath"
|
||||
| "updateById"
|
||||
| "findById"
|
||||
| "findByManySecretPath"
|
||||
| "find"
|
||||
| "findBySecretPathMultiEnv"
|
||||
| "findSecretPathByFolderIds"
|
||||
>;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">;
|
||||
@@ -301,6 +308,10 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
},
|
||||
tx
|
||||
})
|
||||
);
|
||||
@@ -483,6 +494,10 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
},
|
||||
tx
|
||||
})
|
||||
);
|
||||
@@ -947,6 +962,73 @@ export const secretV2BridgeServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getSecretById = async ({ actorId, actor, actorOrgId, actorAuthMethod, secretId }: TGetASecretByIdDTO) => {
|
||||
const secret = await secretDAL.findOneWithTags({
|
||||
[`${TableName.SecretV2}.id` as "id"]: secretId
|
||||
});
|
||||
|
||||
if (!secret) {
|
||||
throw new NotFoundError({
|
||||
message: `Secret with ID '${secretId}' not found`,
|
||||
name: "GetSecretById"
|
||||
});
|
||||
}
|
||||
|
||||
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(secret.projectId, [secret.folderId]);
|
||||
|
||||
if (!folderWithPath) {
|
||||
throw new NotFoundError({
|
||||
message: `Folder with id '${secret.folderId}' not found`,
|
||||
name: "GetSecretById"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: secret.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: folderWithPath.environmentSlug,
|
||||
secretPath: folderWithPath.path,
|
||||
secretName: secret.key,
|
||||
secretTags: secret.tags.map((i) => i.slug)
|
||||
})
|
||||
);
|
||||
|
||||
if (secret.type === SecretType.Personal && secret.userId !== actorId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "You are not allowed to access this secret",
|
||||
name: "GetSecretById"
|
||||
});
|
||||
}
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: secret.projectId
|
||||
});
|
||||
|
||||
const secretValue = secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||
: "";
|
||||
|
||||
const secretComment = secret.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
|
||||
: "";
|
||||
|
||||
return reshapeBridgeSecret(secret.projectId, folderWithPath.environmentSlug, folderWithPath.path, {
|
||||
...secret,
|
||||
value: secretValue,
|
||||
comment: secretComment
|
||||
});
|
||||
};
|
||||
|
||||
const getSecretByName = async ({
|
||||
actorId,
|
||||
actor,
|
||||
@@ -1230,6 +1312,10 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
},
|
||||
tx
|
||||
})
|
||||
);
|
||||
@@ -1490,6 +1576,10 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
},
|
||||
resourceMetadataDAL
|
||||
});
|
||||
updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
|
||||
@@ -1522,6 +1612,10 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
},
|
||||
tx
|
||||
});
|
||||
updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
|
||||
@@ -1689,14 +1783,19 @@ export const secretV2BridgeServiceFactory = ({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: folder.projectId
|
||||
});
|
||||
const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] });
|
||||
return secretVersions.map((el) =>
|
||||
reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
|
||||
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors(secretId, folder.projectId, {
|
||||
offset,
|
||||
limit,
|
||||
sort: [["createdAt", "desc"]]
|
||||
});
|
||||
|
||||
return secretVersions.map((el) => {
|
||||
return reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
|
||||
...el,
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// this is a backfilling API for secret references
|
||||
@@ -1956,6 +2055,10 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretTagDAL,
|
||||
resourceMetadataDAL,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
},
|
||||
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
||||
return {
|
||||
type: doc.type,
|
||||
@@ -1982,6 +2085,10 @@ export const secretV2BridgeServiceFactory = ({
|
||||
tx,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
actor: {
|
||||
type: actor,
|
||||
actorId
|
||||
},
|
||||
inputSecrets: locallyUpdatedSecrets.map((doc) => {
|
||||
return {
|
||||
filter: {
|
||||
@@ -2204,6 +2311,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
getSecretsCountMultiEnv,
|
||||
getSecretsMultiEnv,
|
||||
getSecretReferenceTree,
|
||||
getSecretsByFolderMappings
|
||||
getSecretsByFolderMappings,
|
||||
getSecretById
|
||||
};
|
||||
};
|
||||
|
||||
@@ -168,6 +168,10 @@ export type TFnSecretBulkInsert = {
|
||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">;
|
||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
actor?: {
|
||||
type: string;
|
||||
actorId: string;
|
||||
};
|
||||
};
|
||||
|
||||
type TRequireReferenceIfValue =
|
||||
@@ -192,6 +196,10 @@ export type TFnSecretBulkUpdate = {
|
||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">;
|
||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
actor?: {
|
||||
type: string;
|
||||
actorId: string;
|
||||
};
|
||||
tx?: Knex;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
|
||||
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 { QueueName } from "@app/queue";
|
||||
|
||||
@@ -119,11 +120,67 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
||||
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 {
|
||||
...secretVersionV2Orm,
|
||||
pruneExcessVersions,
|
||||
findLatestVersionMany,
|
||||
bulkUpdate,
|
||||
findLatestVersionByFolderId
|
||||
findLatestVersionByFolderId,
|
||||
findVersionsBySecretIdWithActors
|
||||
};
|
||||
};
|
||||
|
||||
@@ -579,6 +579,7 @@ export const fnSecretBulkInsert = async ({
|
||||
[`${TableName.Secret}Id` as const]: newSecretGroupByBlindIndex[secretBlindIndex as string][0].id
|
||||
}))
|
||||
);
|
||||
|
||||
const secretVersions = await secretVersionDAL.insertMany(
|
||||
sanitizedInputSecrets.map((el) => ({
|
||||
...el,
|
||||
|
||||
@@ -61,6 +61,7 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TWebhookDALFactory } from "../webhook/webhook-dal";
|
||||
import { fnTriggerWebhook } from "../webhook/webhook-fns";
|
||||
import { WebhookEvents } from "../webhook/webhook-types";
|
||||
import { TSecretDALFactory } from "./secret-dal";
|
||||
import { interpolateSecrets } from "./secret-fns";
|
||||
import {
|
||||
@@ -623,7 +624,14 @@ export const secretQueueFactory = ({
|
||||
await queueService.queue(
|
||||
QueueName.SecretWebhook,
|
||||
QueueJobs.SecWebhook,
|
||||
{ environment, projectId, secretPath },
|
||||
{
|
||||
type: WebhookEvents.SecretModified,
|
||||
payload: {
|
||||
environment,
|
||||
projectId,
|
||||
secretPath
|
||||
}
|
||||
},
|
||||
{
|
||||
jobId: `secret-webhook-${environment}-${projectId}-${secretPath}`,
|
||||
removeOnFail: { count: 5 },
|
||||
@@ -1055,6 +1063,8 @@ export const secretQueueFactory = ({
|
||||
|
||||
const organization = await orgDAL.findOrgByProjectId(projectId);
|
||||
const project = await projectDAL.findById(projectId);
|
||||
const secret = await secretV2BridgeDAL.findById(data.secretId);
|
||||
const [folder] = await folderDAL.findSecretPathByFolderIds(project.id, [secret.folderId]);
|
||||
|
||||
if (!organization) {
|
||||
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`);
|
||||
@@ -1083,6 +1093,19 @@ export const secretQueueFactory = ({
|
||||
organizationName: organization.name
|
||||
}
|
||||
});
|
||||
|
||||
await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, {
|
||||
type: WebhookEvents.SecretReminderExpired,
|
||||
payload: {
|
||||
projectName: project.name,
|
||||
projectId: project.id,
|
||||
secretPath: folder?.path,
|
||||
environment: folder?.environmentSlug || "",
|
||||
reminderNote: data.note,
|
||||
secretName: secret?.key,
|
||||
secretId: data.secretId
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const startSecretV2Migration = async (projectId: string) => {
|
||||
@@ -1490,14 +1513,17 @@ export const secretQueueFactory = ({
|
||||
queueService.start(QueueName.SecretWebhook, async (job) => {
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: job.data.projectId
|
||||
projectId: job.data.payload.projectId
|
||||
});
|
||||
|
||||
await fnTriggerWebhook({
|
||||
...job.data,
|
||||
projectId: job.data.payload.projectId,
|
||||
environment: job.data.payload.environment,
|
||||
secretPath: job.data.payload.secretPath || "/",
|
||||
projectEnvDAL,
|
||||
webhookDAL,
|
||||
projectDAL,
|
||||
webhookDAL,
|
||||
event: job.data,
|
||||
secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,6 +71,7 @@ import {
|
||||
TDeleteManySecretRawDTO,
|
||||
TDeleteSecretDTO,
|
||||
TDeleteSecretRawDTO,
|
||||
TGetASecretByIdRawDTO,
|
||||
TGetASecretDTO,
|
||||
TGetASecretRawDTO,
|
||||
TGetSecretAccessListDTO,
|
||||
@@ -95,7 +96,7 @@ type TSecretServiceFactoryDep = {
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
folderDAL: Pick<
|
||||
TSecretFolderDALFactory,
|
||||
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find"
|
||||
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findSecretPathByFolderIds"
|
||||
>;
|
||||
secretV2BridgeService: TSecretV2BridgeServiceFactory;
|
||||
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
||||
@@ -1382,6 +1383,18 @@ export const secretServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getSecretByIdRaw = async ({ secretId, actorId, actor, actorOrgId, actorAuthMethod }: TGetASecretByIdRawDTO) => {
|
||||
const secret = await secretV2BridgeService.getSecretById({
|
||||
secretId,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
});
|
||||
|
||||
return secret;
|
||||
};
|
||||
|
||||
const getSecretByNameRaw = async ({
|
||||
type,
|
||||
path,
|
||||
@@ -3088,6 +3101,7 @@ export const secretServiceFactory = ({
|
||||
getSecretsRawMultiEnv,
|
||||
getSecretReferenceTree,
|
||||
getSecretsRawByFolderMappings,
|
||||
getSecretAccessList
|
||||
getSecretAccessList,
|
||||
getSecretByIdRaw
|
||||
};
|
||||
};
|
||||
|
||||
@@ -121,6 +121,10 @@ export type TGetASecretDTO = {
|
||||
version?: number;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetASecretByIdDTO = {
|
||||
secretId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateBulkSecretDTO = {
|
||||
path: string;
|
||||
environment: string;
|
||||
@@ -213,6 +217,10 @@ export type TGetASecretRawDTO = {
|
||||
projectId?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetASecretByIdRawDTO = {
|
||||
secretId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateSecretRawDTO = TProjectPermission & {
|
||||
secretName: string;
|
||||
secretPath: string;
|
||||
|
||||
@@ -50,6 +50,7 @@ const buildSlackPayload = (notification: TSlackNotification) => {
|
||||
const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
|
||||
*Environment*: ${payload.environment}
|
||||
*Secret path*: ${payload.secretPath || "/"}
|
||||
*Secret Key${payload.secretKeys.length > 1 ? "s" : ""}*: ${payload.secretKeys.join(", ")}
|
||||
|
||||
View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId}/approval?requestId=${
|
||||
payload.requestId
|
||||
|
||||
@@ -62,6 +62,7 @@ export type TSlackNotification =
|
||||
secretPath: string;
|
||||
requestId: string;
|
||||
projectId: string;
|
||||
secretKeys: string[];
|
||||
};
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { logger } from "@app/lib/logger";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TWebhookDALFactory } from "./webhook-dal";
|
||||
import { WebhookType } from "./webhook-types";
|
||||
import { TWebhookPayloads, WebhookEvents, WebhookType } from "./webhook-types";
|
||||
|
||||
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
|
||||
|
||||
@@ -54,29 +54,64 @@ export const triggerWebhookRequest = async (
|
||||
return req;
|
||||
};
|
||||
|
||||
export const getWebhookPayload = (
|
||||
eventName: string,
|
||||
details: {
|
||||
workspaceName: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
type?: string | null;
|
||||
export const getWebhookPayload = (event: TWebhookPayloads) => {
|
||||
if (event.type === WebhookEvents.SecretModified) {
|
||||
const { projectName, projectId, environment, secretPath, type } = event.payload;
|
||||
|
||||
switch (type) {
|
||||
case WebhookType.SLACK:
|
||||
return {
|
||||
text: "A secret value has been added or modified.",
|
||||
attachments: [
|
||||
{
|
||||
color: "#E7F256",
|
||||
fields: [
|
||||
{
|
||||
title: "Project",
|
||||
value: projectName,
|
||||
short: false
|
||||
},
|
||||
{
|
||||
title: "Environment",
|
||||
value: environment,
|
||||
short: false
|
||||
},
|
||||
{
|
||||
title: "Secret Path",
|
||||
value: secretPath,
|
||||
short: false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
case WebhookType.GENERAL:
|
||||
default:
|
||||
return {
|
||||
event: event.type,
|
||||
project: {
|
||||
workspaceId: projectId,
|
||||
projectName,
|
||||
environment,
|
||||
secretPath
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
) => {
|
||||
const { workspaceName, workspaceId, environment, secretPath, type } = details;
|
||||
|
||||
const { projectName, projectId, environment, secretPath, type, reminderNote, secretName } = event.payload;
|
||||
|
||||
switch (type) {
|
||||
case WebhookType.SLACK:
|
||||
return {
|
||||
text: "A secret value has been added or modified.",
|
||||
text: "You have a secret reminder",
|
||||
attachments: [
|
||||
{
|
||||
color: "#E7F256",
|
||||
fields: [
|
||||
{
|
||||
title: "Project",
|
||||
value: workspaceName,
|
||||
value: projectName,
|
||||
short: false
|
||||
},
|
||||
{
|
||||
@@ -88,6 +123,16 @@ export const getWebhookPayload = (
|
||||
title: "Secret Path",
|
||||
value: secretPath,
|
||||
short: false
|
||||
},
|
||||
{
|
||||
title: "Secret Name",
|
||||
value: secretName,
|
||||
short: false
|
||||
},
|
||||
{
|
||||
title: "Reminder Note",
|
||||
value: reminderNote,
|
||||
short: false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -96,11 +141,14 @@ export const getWebhookPayload = (
|
||||
case WebhookType.GENERAL:
|
||||
default:
|
||||
return {
|
||||
event: eventName,
|
||||
event: event.type,
|
||||
project: {
|
||||
workspaceId,
|
||||
workspaceId: projectId,
|
||||
projectName,
|
||||
environment,
|
||||
secretPath
|
||||
secretPath,
|
||||
secretName,
|
||||
reminderNote
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -110,6 +158,7 @@ export type TFnTriggerWebhookDTO = {
|
||||
projectId: string;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
event: TWebhookPayloads;
|
||||
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
@@ -124,8 +173,9 @@ export const fnTriggerWebhook = async ({
|
||||
projectId,
|
||||
webhookDAL,
|
||||
projectEnvDAL,
|
||||
projectDAL,
|
||||
secretManagerDecryptor
|
||||
event,
|
||||
secretManagerDecryptor,
|
||||
projectDAL
|
||||
}: TFnTriggerWebhookDTO) => {
|
||||
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
|
||||
const toBeTriggeredHooks = webhooks.filter(
|
||||
@@ -134,21 +184,20 @@ export const fnTriggerWebhook = async ({
|
||||
);
|
||||
if (!toBeTriggeredHooks.length) return;
|
||||
logger.info({ environment, secretPath, projectId }, "Secret webhook job started");
|
||||
const project = await projectDAL.findById(projectId);
|
||||
let { projectName } = event.payload;
|
||||
if (!projectName) {
|
||||
const project = await projectDAL.findById(event.payload.projectId);
|
||||
projectName = project.name;
|
||||
}
|
||||
|
||||
const webhooksTriggered = await Promise.allSettled(
|
||||
toBeTriggeredHooks.map((hook) =>
|
||||
triggerWebhookRequest(
|
||||
hook,
|
||||
secretManagerDecryptor,
|
||||
getWebhookPayload("secrets.modified", {
|
||||
workspaceName: project.name,
|
||||
workspaceId: projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
type: hook.type
|
||||
})
|
||||
)
|
||||
)
|
||||
toBeTriggeredHooks.map((hook) => {
|
||||
const formattedEvent = {
|
||||
type: event.type,
|
||||
payload: { ...event.payload, type: hook.type, projectName }
|
||||
} as TWebhookPayloads;
|
||||
return triggerWebhookRequest(hook, secretManagerDecryptor, getWebhookPayload(formattedEvent));
|
||||
})
|
||||
);
|
||||
|
||||
// filter hooks by status
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
TDeleteWebhookDTO,
|
||||
TListWebhookDTO,
|
||||
TTestWebhookDTO,
|
||||
TUpdateWebhookDTO
|
||||
TUpdateWebhookDTO,
|
||||
WebhookEvents
|
||||
} from "./webhook-types";
|
||||
|
||||
type TWebhookServiceFactoryDep = {
|
||||
@@ -144,12 +145,15 @@ export const webhookServiceFactory = ({
|
||||
await triggerWebhookRequest(
|
||||
webhook,
|
||||
(value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(),
|
||||
getWebhookPayload("test", {
|
||||
workspaceName: project.name,
|
||||
workspaceId: webhook.projectId,
|
||||
environment: webhook.environment.slug,
|
||||
secretPath: webhook.secretPath,
|
||||
type: webhook.type
|
||||
getWebhookPayload({
|
||||
type: "test" as WebhookEvents.SecretModified,
|
||||
payload: {
|
||||
projectName: project.name,
|
||||
projectId: webhook.projectId,
|
||||
environment: webhook.environment.slug,
|
||||
secretPath: webhook.secretPath,
|
||||
type: webhook.type
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
|
||||
@@ -30,3 +30,36 @@ export enum WebhookType {
|
||||
GENERAL = "general",
|
||||
SLACK = "slack"
|
||||
}
|
||||
|
||||
export enum WebhookEvents {
|
||||
SecretModified = "secrets.modified",
|
||||
SecretReminderExpired = "secrets.reminder-expired",
|
||||
TestEvent = "test"
|
||||
}
|
||||
|
||||
type TWebhookSecretModifiedEventPayload = {
|
||||
type: WebhookEvents.SecretModified;
|
||||
payload: {
|
||||
projectName?: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
type?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
type TWebhookSecretReminderEventPayload = {
|
||||
type: WebhookEvents.SecretReminderExpired;
|
||||
payload: {
|
||||
projectName?: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
type?: string | null;
|
||||
secretName: string;
|
||||
secretId: string;
|
||||
reminderNote?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type TWebhookPayloads = TWebhookSecretModifiedEventPayload | TWebhookSecretReminderEventPayload;
|
||||
|
||||
Reference in New Issue
Block a user