feature: deep search for secrets dashboard

This commit is contained in:
Scott Wilson
2024-10-29 15:08:19 -07:00
parent 1309f30af9
commit 21ea7dd317
30 changed files with 1538 additions and 238 deletions
@@ -14,6 +14,7 @@ import {
} from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { TFindSecretsByFolderIdsFilter } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
export type TSecretV2BridgeDALFactory = ReturnType<typeof secretV2BridgeDALFactory>;
@@ -339,14 +340,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
folderIds: string[],
userId?: string,
tx?: Knex,
filters?: {
limit?: number;
offset?: number;
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
search?: string;
tagSlugs?: string[];
}
filters?: TFindSecretsByFolderIdsFilter
) => {
try {
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
@@ -356,14 +350,20 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
}
const query = (tx || db.replicaNode())(TableName.SecretV2)
.whereIn("folderId", folderIds)
.whereIn(`${TableName.SecretV2}.folderId`, folderIds)
.where((bd) => {
if (filters?.search) {
void bd.whereILike("key", `%${filters?.search}%`);
if (filters?.includeTagsInSearch) {
void bd
.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`)
.orWhereILike(`${TableName.SecretTag}.slug`, `%${filters?.search}%`);
} else {
void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`);
}
}
})
.where((bd) => {
void bd.whereNull("userId").orWhere({ userId: userId || null });
void bd.whereNull(`${TableName.SecretV2}.userId`).orWhere({ userId: userId || null });
})
.leftJoin(
TableName.SecretV2JnTag,
@@ -385,7 +385,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.where((bd) => {
const slugs = filters?.tagSlugs?.filter(Boolean);
if (slugs && slugs.length > 0) {
void bd.whereIn("slug", slugs);
void bd.whereIn(`${TableName.SecretTag}.slug`, slugs);
}
})
.orderBy(
@@ -43,6 +43,7 @@ import {
TGetASecretDTO,
TGetSecretReferencesTreeDTO,
TGetSecretsDTO,
TGetSecretsRawByFolderMappingsDTO,
TGetSecretVersionsDTO,
TMoveSecretsDTO,
TSecretReference,
@@ -652,6 +653,56 @@ export const secretV2BridgeServiceFactory = ({
return count;
};
const getSecretsByFolderMappings = async (
{ projectId, userId, filters, folderMappings }: TGetSecretsRawByFolderMappingsDTO,
projectPermission: Awaited<ReturnType<typeof permissionService.getProjectPermission>>["permission"]
) => {
const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId);
const secrets = await secretDAL.findByFolderIds(
folderMappings.map((folderMapping) => folderMapping.folderId),
userId,
undefined,
filters
);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedSecrets = secrets
.filter((el) =>
projectPermission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: groupedFolderMappings[el.folderId][0].environment,
secretPath: groupedFolderMappings[el.folderId][0].path,
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
})
)
)
.map((secret) =>
reshapeBridgeSecret(
projectId,
groupedFolderMappings[secret.folderId][0].environment,
groupedFolderMappings[secret.folderId][0].path,
{
...secret,
value: secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: "",
comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: ""
}
)
);
return decryptedSecrets;
};
// get secrets for multiple envs
const getSecretsMultiEnv = async ({
actorId,
@@ -678,59 +729,28 @@ export const secretV2BridgeServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
}
let paths: { folderId: string; path: string; environment: string }[] = [];
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
if (!folders.length) {
return [];
}
paths = folders.map((folder) => ({ folderId: folder.id, path, environment: folder.environment.slug }));
const folderMappings = folders.map((folder) => ({
folderId: folder.id,
path,
environment: folder.environment.slug
}));
const groupedPaths = groupBy(paths, (p) => p.folderId);
const secrets = await secretDAL.findByFolderIds(
paths.map((p) => p.folderId),
actorId,
undefined,
params
const decryptedSecrets = await getSecretsByFolderMappings(
{
projectId,
folderMappings,
filters: params,
userId: actorId
},
permission
);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedSecrets = secrets
.filter((el) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: groupedPaths[el.folderId][0].environment,
secretPath: groupedPaths[el.folderId][0].path,
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
})
)
)
.map((secret) =>
reshapeBridgeSecret(
projectId,
groupedPaths[secret.folderId][0].environment,
groupedPaths[secret.folderId][0].path,
{
...secret,
value: secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: "",
comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: ""
}
)
);
return decryptedSecrets;
};
@@ -2027,6 +2047,7 @@ export const secretV2BridgeServiceFactory = ({
getSecretsCount,
getSecretsCountMultiEnv,
getSecretsMultiEnv,
getSecretReferenceTree
getSecretReferenceTree,
getSecretsByFolderMappings
};
};
@@ -285,3 +285,20 @@ export type TGetSecretReferencesTreeDTO = {
environment: string;
secretPath: string;
} & Omit<TProjectPermission, "projectId">;
export type TFindSecretsByFolderIdsFilter = {
limit?: number;
offset?: number;
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
search?: string;
tagSlugs?: string[];
includeTagsInSearch?: boolean;
};
export type TGetSecretsRawByFolderMappingsDTO = {
projectId: string;
folderMappings: { folderId: string; path: string; environment: string }[];
userId: string;
filters: TFindSecretsByFolderIdsFilter;
};