mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 07:26:45 +00:00
Feat: Recursively get all secrets from inside path
This commit is contained in:
@@ -157,11 +157,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug),
|
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug),
|
||||||
environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment),
|
environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment),
|
||||||
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath),
|
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath),
|
||||||
recursive: z
|
deep: z
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
.default("false")
|
.default("false")
|
||||||
.transform((value) => value === "true")
|
.transform((value) => value === "true")
|
||||||
.describe(RAW_SECRETS.LIST.recursive),
|
.describe(RAW_SECRETS.LIST.deep),
|
||||||
include_imports: z
|
include_imports: z
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
.default("false")
|
.default("false")
|
||||||
@@ -230,7 +230,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
projectId: workspaceId,
|
projectId: workspaceId,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
includeImports: req.query.include_imports,
|
includeImports: req.query.include_imports,
|
||||||
recursive: req.query.recursive
|
deep: req.query.deep
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
@@ -608,6 +608,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
workspaceId: z.string().trim(),
|
workspaceId: z.string().trim(),
|
||||||
environment: z.string().trim(),
|
environment: z.string().trim(),
|
||||||
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
|
deep: z
|
||||||
|
.enum(["true", "false"])
|
||||||
|
.default("false")
|
||||||
|
.transform((value) => value === "true"),
|
||||||
include_imports: z
|
include_imports: z
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
.default("false")
|
.default("false")
|
||||||
@@ -621,6 +625,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
_id: z.string(),
|
_id: z.string(),
|
||||||
workspace: z.string(),
|
workspace: z.string(),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
|
secretPath: z.string().optional(),
|
||||||
tags: SecretTagsSchema.pick({
|
tags: SecretTagsSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
@@ -660,7 +665,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
environment: req.query.environment,
|
environment: req.query.environment,
|
||||||
projectId: req.query.workspaceId,
|
projectId: req.query.workspaceId,
|
||||||
path: req.query.secretPath,
|
path: req.query.secretPath,
|
||||||
includeImports: req.query.include_imports
|
includeImports: req.query.include_imports,
|
||||||
|
deep: req.query.deep
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import { subject } from "@casl/ability";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
TSecrets
|
TSecrets
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import {
|
import {
|
||||||
buildSecretBlindIndexFromName,
|
buildSecretBlindIndexFromName,
|
||||||
@@ -20,6 +22,7 @@ import { BadRequestError } from "@app/lib/errors";
|
|||||||
import { groupBy, unique } from "@app/lib/fn";
|
import { groupBy, unique } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
|
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
@@ -54,8 +57,25 @@ type TRecursivelyFetchSecretsFromFoldersArg = {
|
|||||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "find">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "find">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const recursivelyGetSecretPaths = ({ folderDAL, projectEnvDAL }: TRecursivelyFetchSecretsFromFoldersArg) => {
|
type TGetPathsDTO = {
|
||||||
const getPaths = async (projectId: string, environment: string, currentPath: string) => {
|
projectId: string;
|
||||||
|
environment: string;
|
||||||
|
currentPath: string;
|
||||||
|
|
||||||
|
auth: {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
actorOrgId: string | undefined;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const recursivelyGetSecretPaths = ({
|
||||||
|
folderDAL,
|
||||||
|
projectEnvDAL,
|
||||||
|
permissionService
|
||||||
|
}: TRecursivelyFetchSecretsFromFoldersArg) => {
|
||||||
|
const getPaths = async ({ projectId, environment, currentPath }: Omit<TGetPathsDTO, "auth">) => {
|
||||||
let secretPaths: string[] = [];
|
let secretPaths: string[] = [];
|
||||||
|
|
||||||
// Get secrets in the current folder.
|
// Get secrets in the current folder.
|
||||||
@@ -93,7 +113,7 @@ export const recursivelyGetSecretPaths = ({ folderDAL, projectEnvDAL }: TRecursi
|
|||||||
// Ensure the path is correctly formatted for the next level.
|
// Ensure the path is correctly formatted for the next level.
|
||||||
const subFolderPath = `${currentPath}${currentPath !== "/" ? "/" : ""}${folder.name}`;
|
const subFolderPath = `${currentPath}${currentPath !== "/" ? "/" : ""}${folder.name}`;
|
||||||
|
|
||||||
return getPaths(projectId, environment, subFolderPath);
|
return getPaths({ projectId, environment, currentPath: subFolderPath });
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -107,7 +127,32 @@ export const recursivelyGetSecretPaths = ({ folderDAL, projectEnvDAL }: TRecursi
|
|||||||
return secretPaths;
|
return secretPaths;
|
||||||
};
|
};
|
||||||
|
|
||||||
return getPaths;
|
return async ({ projectId, environment, currentPath, auth }: TGetPathsDTO) => {
|
||||||
|
const paths = await getPaths({ projectId, environment, currentPath });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
auth.actor,
|
||||||
|
auth.actorId,
|
||||||
|
projectId,
|
||||||
|
auth.actorAuthMethod,
|
||||||
|
auth.actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
const allowedPaths = paths.filter((p) =>
|
||||||
|
// if its service token allow full access over imported one
|
||||||
|
auth.actor === ActorType.SERVICE
|
||||||
|
? true
|
||||||
|
: permission.can(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment,
|
||||||
|
secretPath: p
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return allowedPaths;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type TInterpolateSecretArg = {
|
type TInterpolateSecretArg = {
|
||||||
@@ -275,7 +320,10 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
|
|||||||
return expandSecrets;
|
return expandSecrets;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const decryptSecretRaw = (secret: TSecrets & { workspace: string; environment: string }, key: string) => {
|
export const decryptSecretRaw = (
|
||||||
|
secret: TSecrets & { workspace: string; environment: string; secretPath?: string },
|
||||||
|
key: string
|
||||||
|
) => {
|
||||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: secret.secretKeyCiphertext,
|
ciphertext: secret.secretKeyCiphertext,
|
||||||
iv: secret.secretKeyIV,
|
iv: secret.secretKeyIV,
|
||||||
@@ -303,6 +351,7 @@ export const decryptSecretRaw = (secret: TSecrets & { workspace: string; environ
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
secretKey,
|
secretKey,
|
||||||
|
secretPath: secret.secretPath,
|
||||||
workspace: secret.workspace,
|
workspace: secret.workspace,
|
||||||
environment: secret.environment,
|
environment: secret.environment,
|
||||||
secretValue,
|
secretValue,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable no-unreachable-loop */
|
||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
import { SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, SecretType } from "@app/db/schemas";
|
import { SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, SecretType } from "@app/db/schemas";
|
||||||
@@ -437,51 +439,98 @@ export const secretServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
includeImports
|
includeImports,
|
||||||
|
deep
|
||||||
}: TGetSecretsDTO) => {
|
}: TGetSecretsDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const importsArray: Awaited<ReturnType<typeof fnSecretsFromImports>> = [];
|
||||||
actor,
|
const secretsArray: (Awaited<ReturnType<typeof secretDAL.findByFolderId>>[number] & {
|
||||||
actorId,
|
secretPath: string;
|
||||||
projectId,
|
environment: string;
|
||||||
actorAuthMethod,
|
workspace: string;
|
||||||
actorOrgId
|
})[] = [];
|
||||||
);
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
let paths = [path];
|
||||||
if (!folder) return { secrets: [], imports: [] };
|
|
||||||
const folderId = folder.id;
|
|
||||||
|
|
||||||
const secrets = await secretDAL.findByFolderId(folderId, actorId);
|
if (deep) {
|
||||||
|
const getPaths = recursivelyGetSecretPaths({
|
||||||
|
permissionService,
|
||||||
|
folderDAL,
|
||||||
|
projectEnvDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
const deepPaths = await getPaths({
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
currentPath: path,
|
||||||
|
auth: {
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
paths = deepPaths.length === 0 ? paths : deepPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const currentPath of paths) {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: currentPath })
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDAL.findBySecretPath(projectId, environment, currentPath);
|
||||||
|
if (!folder) return { secrets: [], imports: [] };
|
||||||
|
const folderId = folder.id;
|
||||||
|
|
||||||
|
const secrets = await secretDAL.findByFolderId(folderId, actorId);
|
||||||
|
if (includeImports) {
|
||||||
|
const secretImports = await secretImportDAL.find({ folderId });
|
||||||
|
const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
|
||||||
|
// if its service token allow full access over imported one
|
||||||
|
actor === ActorType.SERVICE
|
||||||
|
? true
|
||||||
|
: permission.can(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment: importEnv.slug,
|
||||||
|
secretPath: importPath
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const importedSecrets = await fnSecretsFromImports({
|
||||||
|
allowedImports,
|
||||||
|
secretDAL,
|
||||||
|
folderDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
secretsArray.push(
|
||||||
|
...secrets.map((secret) => ({ ...secret, secretPath: currentPath, workspace: projectId, environment }))
|
||||||
|
);
|
||||||
|
importsArray.push(...importedSecrets);
|
||||||
|
}
|
||||||
|
secretsArray.push(
|
||||||
|
...secrets.map((secret) => ({ ...secret, secretPath: currentPath, workspace: projectId, environment }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (includeImports) {
|
if (includeImports) {
|
||||||
const secretImports = await secretImportDAL.find({ folderId });
|
|
||||||
const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
|
|
||||||
// if its service token allow full access over imported one
|
|
||||||
actor === ActorType.SERVICE
|
|
||||||
? true
|
|
||||||
: permission.can(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment: importEnv.slug,
|
|
||||||
secretPath: importPath
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const importedSecrets = await fnSecretsFromImports({
|
|
||||||
allowedImports,
|
|
||||||
secretDAL,
|
|
||||||
folderDAL
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })),
|
secrets: secretsArray,
|
||||||
imports: importedSecrets
|
imports: importsArray
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })) };
|
|
||||||
|
return {
|
||||||
|
secrets: secretsArray
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSecretByName = async ({
|
const getSecretByName = async ({
|
||||||
@@ -802,70 +851,32 @@ export const secretServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
environment,
|
environment,
|
||||||
includeImports,
|
includeImports,
|
||||||
recursive
|
deep
|
||||||
}: TGetSecretsRawDTO) => {
|
}: TGetSecretsRawDTO) => {
|
||||||
const botKey = await projectBotService.getBotKey(projectId);
|
const botKey = await projectBotService.getBotKey(projectId);
|
||||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||||
|
|
||||||
let secrets: Awaited<ReturnType<typeof getSecrets>>["secrets"];
|
const { secrets, imports } = await getSecrets({
|
||||||
let imports: Awaited<ReturnType<typeof getSecrets>>["imports"];
|
actorId,
|
||||||
|
projectId,
|
||||||
if (recursive) {
|
environment,
|
||||||
const getPaths = recursivelyGetSecretPaths({
|
actor,
|
||||||
permissionService,
|
actorOrgId,
|
||||||
folderDAL,
|
actorAuthMethod,
|
||||||
projectEnvDAL
|
path,
|
||||||
});
|
includeImports,
|
||||||
|
deep
|
||||||
const paths = await getPaths(projectId, environment, path);
|
});
|
||||||
|
|
||||||
const result = await Promise.all(
|
|
||||||
paths.map(async (currentPath) => {
|
|
||||||
const secs = await getSecrets({
|
|
||||||
actorId,
|
|
||||||
projectId,
|
|
||||||
environment,
|
|
||||||
actor,
|
|
||||||
actorOrgId,
|
|
||||||
actorAuthMethod,
|
|
||||||
path: currentPath,
|
|
||||||
includeImports
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
secrets: {
|
|
||||||
...secs.secrets,
|
|
||||||
secretPath: currentPath
|
|
||||||
},
|
|
||||||
imports: secs.imports
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
secrets = result.flatMap((el) => el.secrets);
|
|
||||||
imports = result.flatMap((el) => el.imports || []);
|
|
||||||
} else {
|
|
||||||
const result = await getSecrets({
|
|
||||||
actorId,
|
|
||||||
projectId,
|
|
||||||
environment,
|
|
||||||
actor,
|
|
||||||
actorOrgId,
|
|
||||||
actorAuthMethod,
|
|
||||||
path,
|
|
||||||
includeImports
|
|
||||||
});
|
|
||||||
|
|
||||||
secrets = result.secrets;
|
|
||||||
imports = result.imports;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
secrets: secrets.map((el) => decryptSecretRaw(el, botKey)),
|
secrets: secrets.map((el) => decryptSecretRaw(el, botKey)),
|
||||||
imports: (imports || [])?.map(({ secrets: importedSecrets, ...el }) => ({
|
imports: (imports || [])?.map(({ secrets: importedSecrets, ...el }) => ({
|
||||||
...el,
|
...el,
|
||||||
secrets: importedSecrets.map((sec) =>
|
secrets: importedSecrets.map((sec) =>
|
||||||
decryptSecretRaw({ ...sec, environment: el.environment, workspace: projectId }, botKey)
|
decryptSecretRaw(
|
||||||
|
{ ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath },
|
||||||
|
botKey
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user