Merge pull request #2645 from scott-ray-wilson/secrets-quick-search

Feature: Secrets Dashboard Quick/Deep Search
This commit is contained in:
Scott Wilson
2024-10-31 08:41:54 -07:00
committed by GitHub
30 changed files with 1541 additions and 238 deletions
@@ -9,7 +9,7 @@ import {
} from "@app/ee/services/permission/project-permission";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection } from "@app/lib/types";
import { OrderByDirection, ProjectServiceActor } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@@ -22,6 +22,7 @@ import {
TDeleteDynamicSecretDTO,
TDetailsDynamicSecretDTO,
TGetDynamicSecretsCountDTO,
TListDynamicSecretsByFolderMappingsDTO,
TListDynamicSecretsDTO,
TListDynamicSecretsMultiEnvDTO,
TUpdateDynamicSecretDTO
@@ -454,8 +455,44 @@ export const dynamicSecretServiceFactory = ({
return dynamicSecretCfg;
};
const listDynamicSecretsByFolderIds = async (
{ folderMappings, filters, projectId }: TListDynamicSecretsByFolderMappingsDTO,
actor: ProjectServiceActor
) => {
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
projectId,
actor.authMethod,
actor.orgId
);
const userAccessibleFolderMappings = folderMappings.filter(({ path, environment }) =>
permission.can(
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath: path })
)
);
const groupedFolderMappings = new Map(userAccessibleFolderMappings.map((path) => [path.folderId, path]));
const dynamicSecrets = await dynamicSecretDAL.listDynamicSecretsByFolderIds({
folderIds: userAccessibleFolderMappings.map(({ folderId }) => folderId),
...filters
});
return dynamicSecrets.map((dynamicSecret) => {
const { environment, path } = groupedFolderMappings.get(dynamicSecret.folderId)!;
return {
...dynamicSecret,
environment,
path
};
});
};
// get dynamic secrets for multiple envs
const listDynamicSecretsByFolderIds = async ({
const listDynamicSecretsByEnvs = async ({
actorAuthMethod,
actorOrgId,
actorId,
@@ -521,9 +558,10 @@ export const dynamicSecretServiceFactory = ({
deleteByName,
getDetails,
listDynamicSecretsByEnv,
listDynamicSecretsByFolderIds,
listDynamicSecretsByEnvs,
getDynamicSecretCount,
getCountMultiEnv,
fetchAzureEntraIdUsers
fetchAzureEntraIdUsers,
listDynamicSecretsByFolderIds
};
};
@@ -48,17 +48,27 @@ export type TDetailsDynamicSecretDTO = {
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;
export type TListDynamicSecretsDTO = {
path: string;
environmentSlug: string;
projectSlug?: string;
projectId?: string;
export type ListDynamicSecretsFilters = {
offset?: number;
limit?: number;
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
search?: string;
} & Omit<TProjectPermission, "projectId">;
};
export type TListDynamicSecretsDTO = {
path: string;
environmentSlug: string;
projectSlug?: string;
projectId?: string;
} & ListDynamicSecretsFilters &
Omit<TProjectPermission, "projectId">;
export type TListDynamicSecretsByFolderMappingsDTO = {
projectId: string;
folderMappings: { folderId: string; path: string; environment: string }[];
filters: ListDynamicSecretsFilters;
};
export type TListDynamicSecretsMultiEnvDTO = Omit<
TListDynamicSecretsDTO,
+7
View File
@@ -57,3 +57,10 @@ export enum OrderByDirection {
ASC = "asc",
DESC = "desc"
}
export type ProjectServiceActor = {
type: ActorType;
id: string;
authMethod: ActorAuthMethod;
orgId: string;
};
@@ -20,6 +20,8 @@ import { AuthMode } from "@app/services/auth/auth-type";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
const MAX_DEEP_SEARCH_LIMIT = 500; // arbitrary limit to prevent excessive results
// handle querystring boolean values
const booleanSchema = z
.union([z.boolean(), z.string().trim()])
@@ -34,6 +36,35 @@ const booleanSchema = z
.optional()
.default(true);
const parseSecretPathSearch = (search?: string) => {
if (!search)
return {
searchName: "",
searchPath: ""
};
if (!search.includes("/"))
return {
searchName: search,
searchPath: ""
};
if (search === "/")
return {
searchName: "",
searchPath: "/"
};
const [searchName, ...searchPathSegments] = search.split("/").reverse();
let searchPath = removeTrailingSlash(searchPathSegments.reverse().join("/").toLowerCase());
if (!searchPath.startsWith("/")) searchPath = `/${searchPath}`;
return {
searchName,
searchPath
};
};
export const registerDashboardRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
@@ -134,7 +165,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined;
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>> | undefined;
let dynamicSecrets:
| Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByFolderIds>>
| Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnvs>>
| undefined;
let totalFolderCount: number | undefined;
@@ -218,7 +249,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
});
if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) {
dynamicSecrets = await server.services.dynamicSecret.listDynamicSecretsByFolderIds({
dynamicSecrets = await server.services.dynamicSecret.listDynamicSecretsByEnvs({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -633,4 +664,180 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
};
}
});
server.route({
method: "GET",
url: "/secrets-deep-search",
config: {
rateLimit: secretsLimit
},
schema: {
security: [
{
bearerAuth: []
}
],
querystring: z.object({
projectId: z.string().trim(),
environments: z.string().trim().transform(decodeURIComponent),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
search: z.string().trim().optional(),
tags: z.string().trim().transform(decodeURIComponent).optional()
}),
response: {
200: z.object({
folders: SecretFoldersSchema.extend({ path: z.string() }).array().optional(),
dynamicSecrets: SanitizedDynamicSecretSchema.extend({ path: z.string(), environment: z.string() })
.array()
.optional(),
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
})
.array()
.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secretPath, projectId, search } = req.query;
const environments = req.query.environments.split(",").filter((env) => Boolean(env.trim()));
if (!environments.length) throw new BadRequestError({ message: "One or more environments required" });
const tags = req.query.tags?.split(",").filter((tag) => Boolean(tag.trim())) ?? [];
if (!search && !tags.length) throw new BadRequestError({ message: "Search or tags required" });
const searchHasTags = Boolean(tags.length);
const allFolders = await server.services.folder.getFoldersDeepByEnvs(
{
projectId,
environments,
secretPath
},
req.permission
);
const { searchName, searchPath } = parseSecretPathSearch(search);
const folderMappings = allFolders.map((folder) => ({
folderId: folder.id,
path: folder.path,
environment: folder.environment
}));
const sharedFilters = {
search: searchName,
limit: MAX_DEEP_SEARCH_LIMIT,
orderBy: SecretsOrderBy.Name
};
const secrets = await server.services.secret.getSecretsRawByFolderMappings(
{
projectId,
folderMappings,
filters: {
...sharedFilters,
tagSlugs: tags,
includeTagsInSearch: true
}
},
req.permission
);
const dynamicSecrets = searchHasTags
? []
: await server.services.dynamicSecret.listDynamicSecretsByFolderIds(
{
projectId,
folderMappings,
filters: sharedFilters
},
req.permission
);
for await (const environment of environments) {
const secretCountForEnv = secrets.filter((secret) => secret.environment === environment).length;
if (secretCountForEnv) {
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secretCountForEnv
}
}
});
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: secretCountForEnv,
workspaceId: projectId,
environment,
secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
}
}
}
const sliceQuickSearch = <T>(array: T[]) => array.slice(0, 25);
return {
secrets: sliceQuickSearch(
searchPath ? secrets.filter((secret) => secret.secretPath.endsWith(searchPath)) : secrets
),
dynamicSecrets: sliceQuickSearch(
searchPath
? dynamicSecrets.filter((dynamicSecret) => dynamicSecret.path.endsWith(searchPath))
: dynamicSecrets
),
folders: searchHasTags
? []
: sliceQuickSearch(
allFolders.filter((folder) => {
const [folderName, ...folderPathSegments] = folder.path.split("/").reverse();
const folderPath = folderPathSegments.reverse().join("/").toLowerCase() || "/";
if (searchPath) {
if (searchPath === "/") {
// only show root folders if no folder name search
if (!searchName) return folderPath === searchPath;
// start partial match on root folders
return folderName.toLowerCase().startsWith(searchName.toLowerCase());
}
// support ending partial path match
return (
folderPath.endsWith(searchPath) && folderName.toLowerCase().startsWith(searchName.toLowerCase())
);
}
// no search path, "fuzzy" match all folders
return folderName.toLowerCase().includes(searchName.toLowerCase());
})
)
};
}
});
};
+7 -7
View File
@@ -1,9 +1,9 @@
import { ForbiddenError } from "@casl/ability";
import { FastifyRequest } from "fastify";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ProjectServiceActor } from "@app/lib/types";
import {
TCmekDecryptDTO,
TCmekEncryptDTO,
@@ -23,7 +23,7 @@ type TCmekServiceFactoryDep = {
export type TCmekServiceFactory = ReturnType<typeof cmekServiceFactory>;
export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TCmekServiceFactoryDep) => {
const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: FastifyRequest["permission"]) => {
const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: ProjectServiceActor) => {
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
@@ -43,7 +43,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return cmek;
};
const updateCmekById = async ({ keyId, ...data }: TUpdabteCmekByIdDTO, actor: FastifyRequest["permission"]) => {
const updateCmekById = async ({ keyId, ...data }: TUpdabteCmekByIdDTO, actor: ProjectServiceActor) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
@@ -65,7 +65,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return cmek;
};
const deleteCmekById = async (keyId: string, actor: FastifyRequest["permission"]) => {
const deleteCmekById = async (keyId: string, actor: ProjectServiceActor) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
@@ -89,7 +89,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
const listCmeksByProjectId = async (
{ projectId, ...filters }: TListCmeksByProjectIdDTO,
actor: FastifyRequest["permission"]
actor: ProjectServiceActor
) => {
const { permission } = await permissionService.getProjectPermission(
actor.type,
@@ -106,7 +106,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return { cmeks, totalCount };
};
const cmekEncrypt = async ({ keyId, plaintext }: TCmekEncryptDTO, actor: FastifyRequest["permission"]) => {
const cmekEncrypt = async ({ keyId, plaintext }: TCmekEncryptDTO, actor: ProjectServiceActor) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
@@ -132,7 +132,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return cipherTextBlob.toString("base64");
};
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: FastifyRequest["permission"]) => {
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: ProjectServiceActor) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
@@ -1,9 +1,9 @@
import { ForbiddenError } from "@casl/ability";
import { FastifyRequest } from "fastify";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectServiceActor } from "@app/lib/types";
import { constructGroupOrgMembershipRoleMappings } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-fns";
import { TSyncExternalGroupOrgMembershipRoleMappingsDTO } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-types";
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
@@ -25,7 +25,7 @@ export const externalGroupOrgRoleMappingServiceFactory = ({
permissionService,
orgRoleDAL
}: TExternalGroupOrgRoleMappingServiceFactoryDep) => {
const listExternalGroupOrgRoleMappings = async (actor: FastifyRequest["permission"]) => {
const listExternalGroupOrgRoleMappings = async (actor: ProjectServiceActor) => {
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
@@ -46,7 +46,7 @@ export const externalGroupOrgRoleMappingServiceFactory = ({
const updateExternalGroupOrgRoleMappings = async (
dto: TSyncExternalGroupOrgMembershipRoleMappingsDTO,
actor: FastifyRequest["permission"]
actor: ProjectServiceActor
) => {
const { permission } = await permissionService.getOrgPermission(
actor.type,
@@ -8,6 +8,8 @@ import { ormify, selectAllTableCols } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { TFindFoldersDeepByParentIdsDTO } from "./secret-folder-types";
export const validateFolderName = (folderName: string) => {
const validNameRegex = /^[a-zA-Z0-9-_]+$/;
return validNameRegex.test(folderName);
@@ -444,6 +446,48 @@ export const secretFolderDALFactory = (db: TDbClient) => {
}
};
const findByEnvsDeep = async ({ parentIds }: TFindFoldersDeepByParentIdsDTO, tx?: Knex) => {
try {
const folders = await (tx || db.replicaNode())
.withRecursive("parents", (qb) =>
qb
.select(
selectAllTableCols(TableName.SecretFolder),
db.raw("0 as depth"),
db.raw(`'/' as path`),
db.ref(`${TableName.Environment}.slug`).as("environment")
)
.from(TableName.SecretFolder)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.whereIn(`${TableName.SecretFolder}.id`, parentIds)
.union((un) => {
void un
.select(
selectAllTableCols(TableName.SecretFolder),
db.raw("parents.depth + 1 as depth"),
db.raw(
`CONCAT(
CASE WHEN parents.path = '/' THEN '' ELSE parents.path END,
CASE WHEN ${TableName.SecretFolder}."parentId" is NULL THEN '' ELSE CONCAT('/', secret_folders.name) END
)`
),
db.ref("parents.environment")
)
.from(TableName.SecretFolder)
.join("parents", `${TableName.SecretFolder}.parentId`, "parents.id");
})
)
.select<(TSecretFolders & { path: string; depth: number; environment: string })[]>("*")
.from("parents")
.orderBy("depth")
.orderBy(`name`);
return folders;
} catch (error) {
throw new DatabaseError({ error, name: "FindByEnvsDeep" });
}
};
return {
...secretFolderOrm,
update,
@@ -454,6 +498,7 @@ export const secretFolderDALFactory = (db: TDbClient) => {
findSecretPathByFolderIds,
findClosestFolder,
findByProjectId,
findByMultiEnv
findByMultiEnv,
findByEnvsDeep
};
};
@@ -7,7 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection } from "@app/lib/types";
import { OrderByDirection, ProjectServiceActor } from "@app/lib/types";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
@@ -17,6 +17,7 @@ import {
TDeleteFolderDTO,
TGetFolderByIdDTO,
TGetFolderDTO,
TGetFoldersDeepByEnvsDTO,
TUpdateFolderDTO,
TUpdateManyFoldersDTO
} from "./secret-folder-types";
@@ -511,6 +512,30 @@ export const secretFolderServiceFactory = ({
};
};
const getFoldersDeepByEnvs = async (
{ projectId, environments, secretPath }: TGetFoldersDeepByEnvsDTO,
actor: ProjectServiceActor
) => {
// folder list is allowed to be read by anyone
// permission to check does user have access
await permissionService.getProjectPermission(actor.type, actor.id, projectId, actor.authMethod, actor.orgId);
const envs = await projectEnvDAL.findBySlugs(projectId, environments);
if (!envs.length)
throw new NotFoundError({
message: `Environments '${environments.join(", ")}' not found`,
name: "GetFoldersDeep"
});
const parentFolders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath);
if (!parentFolders.length) return [];
const folders = await folderDAL.findByEnvsDeep({ parentIds: parentFolders.map((parent) => parent.id) });
return folders;
};
return {
createFolder,
updateFolder,
@@ -519,6 +544,7 @@ export const secretFolderServiceFactory = ({
getFolders,
getFolderById,
getProjectFolderCount,
getFoldersMultiEnv
getFoldersMultiEnv,
getFoldersDeepByEnvs
};
};
@@ -47,3 +47,13 @@ export type TGetFolderDTO = {
export type TGetFolderByIdDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetFoldersDeepByEnvsDTO = {
projectId: string;
environments: string[];
secretPath: string;
};
export type TFindFoldersDeepByParentIdsDTO = {
parentIds: string[];
};
@@ -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;
};
+25 -1
View File
@@ -27,6 +27,8 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
import { groupBy, pick } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { ProjectServiceActor } from "@app/lib/types";
import { TGetSecretsRawByFolderMappingsDTO } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { ActorType } from "../auth/auth-type";
import { TProjectDALFactory } from "../project/project-dal";
@@ -2845,6 +2847,27 @@ export const secretServiceFactory = ({
return { message: "Migrating project to new KMS architecture" };
};
const getSecretsRawByFolderMappings = async (
params: Omit<TGetSecretsRawByFolderMappingsDTO, "userId">,
actor: ProjectServiceActor
) => {
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(params.projectId);
if (!shouldUseSecretV2Bridge) throw new BadRequestError({ message: "Project version not supported" });
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
params.projectId,
actor.authMethod,
actor.orgId
);
const secrets = secretV2BridgeService.getSecretsByFolderMappings({ ...params, userId: actor.id }, permission);
return secrets;
};
return {
attachTags,
detachTags,
@@ -2871,6 +2894,7 @@ export const secretServiceFactory = ({
getSecretsCount,
getSecretsCountMultiEnv,
getSecretsRawMultiEnv,
getSecretReferenceTree
getSecretReferenceTree,
getSecretsRawByFolderMappings
};
};