mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 15:27:27 +00:00
fix: refactor secrets overview endpoint to filter envs for secrets with read permissions
This commit is contained in:
@@ -313,23 +313,26 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
projectId,
|
projectId,
|
||||||
path,
|
path,
|
||||||
environmentSlugs,
|
environmentSlugs,
|
||||||
search
|
search,
|
||||||
|
isInternal
|
||||||
}: TListDynamicSecretsMultiEnvDTO) => {
|
}: TListDynamicSecretsMultiEnvDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
if (!isInternal) {
|
||||||
actor,
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actorId,
|
actor,
|
||||||
projectId,
|
actorId,
|
||||||
actorAuthMethod,
|
projectId,
|
||||||
actorOrgId
|
actorAuthMethod,
|
||||||
);
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
// verify user has access to each env in request
|
// verify user has access to each env in request
|
||||||
environmentSlugs.forEach((environmentSlug) =>
|
environmentSlugs.forEach((environmentSlug) =>
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
||||||
if (!folders.length) throw new BadRequestError({ message: "Folders not found" });
|
if (!folders.length) throw new BadRequestError({ message: "Folders not found" });
|
||||||
@@ -434,23 +437,26 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
path,
|
path,
|
||||||
environmentSlugs,
|
environmentSlugs,
|
||||||
projectId,
|
projectId,
|
||||||
|
isInternal,
|
||||||
...params
|
...params
|
||||||
}: TListDynamicSecretsMultiEnvDTO) => {
|
}: TListDynamicSecretsMultiEnvDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
if (!isInternal) {
|
||||||
actor,
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actorId,
|
actor,
|
||||||
projectId,
|
actorId,
|
||||||
actorAuthMethod,
|
projectId,
|
||||||
actorOrgId
|
actorAuthMethod,
|
||||||
);
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
// verify user has access to each env in request
|
// verify user has access to each env in request
|
||||||
environmentSlugs.forEach((environmentSlug) =>
|
environmentSlugs.forEach((environmentSlug) =>
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
||||||
if (!folders.length) throw new BadRequestError({ message: "Folders not found" });
|
if (!folders.length) throw new BadRequestError({ message: "Folders not found" });
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export type TListDynamicSecretsDTO = {
|
|||||||
export type TListDynamicSecretsMultiEnvDTO = Omit<
|
export type TListDynamicSecretsMultiEnvDTO = Omit<
|
||||||
TListDynamicSecretsDTO,
|
TListDynamicSecretsDTO,
|
||||||
"projectId" | "environmentSlug" | "projectSlug"
|
"projectId" | "environmentSlug" | "projectSlug"
|
||||||
> & { projectId: string; environmentSlugs: string[] };
|
> & { projectId: string; environmentSlugs: string[]; isInternal?: boolean };
|
||||||
|
|
||||||
export type TGetDynamicSecretsCountDTO = Omit<TListDynamicSecretsDTO, "projectSlug" | "projectId"> & {
|
export type TGetDynamicSecretsCountDTO = Omit<TListDynamicSecretsDTO, "projectSlug" | "projectId"> & {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas";
|
import { SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas";
|
||||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { DASHBOARD } from "@app/lib/api-docs";
|
import { DASHBOARD } from "@app/lib/api-docs";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
@@ -174,114 +175,135 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (!includeDynamicSecrets && !includeSecrets)
|
||||||
if (includeDynamicSecrets) {
|
return {
|
||||||
// this is the unique count, ie duplicate secrets across envs only count as 1
|
folders,
|
||||||
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
|
totalFolderCount,
|
||||||
|
totalCount: totalFolderCount ?? 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const { permission } = await server.services.permission.getProjectPermission(
|
||||||
|
req.permission.type,
|
||||||
|
req.permission.id,
|
||||||
|
projectId,
|
||||||
|
req.permission.authMethod,
|
||||||
|
req.permission.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
const permissiveEnvs = // filter envs user has access to
|
||||||
|
environments.filter((environment) =>
|
||||||
|
permission.can(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (includeDynamicSecrets) {
|
||||||
|
// this is the unique count, ie duplicate secrets across envs only count as 1
|
||||||
|
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId,
|
||||||
|
search,
|
||||||
|
environmentSlugs: permissiveEnvs,
|
||||||
|
path: secretPath,
|
||||||
|
isInternal: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) {
|
||||||
|
dynamicSecrets = await server.services.dynamicSecret.listDynamicSecretsByFolderIds({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId,
|
projectId,
|
||||||
search,
|
search,
|
||||||
environmentSlugs: environments,
|
orderBy,
|
||||||
path: secretPath
|
orderDirection,
|
||||||
|
environmentSlugs: permissiveEnvs,
|
||||||
|
path: secretPath,
|
||||||
|
limit: remainingLimit,
|
||||||
|
offset: adjustedOffset,
|
||||||
|
isInternal: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) {
|
// get the count of unique dynamic secret names to properly adjust remaining limit
|
||||||
dynamicSecrets = await server.services.dynamicSecret.listDynamicSecretsByFolderIds({
|
const uniqueDynamicSecretsCount = new Set(dynamicSecrets.map((dynamicSecret) => dynamicSecret.name)).size;
|
||||||
actor: req.permission.type,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
projectId,
|
|
||||||
search,
|
|
||||||
orderBy,
|
|
||||||
orderDirection,
|
|
||||||
environmentSlugs: environments,
|
|
||||||
path: secretPath,
|
|
||||||
limit: remainingLimit,
|
|
||||||
offset: adjustedOffset
|
|
||||||
});
|
|
||||||
|
|
||||||
// get the count of unique dynamic secret names to properly adjust remaining limit
|
remainingLimit -= uniqueDynamicSecretsCount;
|
||||||
const uniqueDynamicSecretsCount = new Set(dynamicSecrets.map((dynamicSecret) => dynamicSecret.name)).size;
|
adjustedOffset = 0;
|
||||||
|
} else {
|
||||||
remainingLimit -= uniqueDynamicSecretsCount;
|
adjustedOffset = Math.max(0, adjustedOffset - totalDynamicSecretCount);
|
||||||
adjustedOffset = 0;
|
|
||||||
} else {
|
|
||||||
adjustedOffset = Math.max(0, adjustedOffset - totalDynamicSecretCount);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (includeSecrets) {
|
if (includeSecrets) {
|
||||||
// this is the unique count, ie duplicate secrets across envs only count as 1
|
// this is the unique count, ie duplicate secrets across envs only count as 1
|
||||||
totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({
|
totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
environments: permissiveEnvs,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
projectId,
|
||||||
|
path: secretPath,
|
||||||
|
search,
|
||||||
|
isInternal: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
|
||||||
|
secrets = await server.services.secret.getSecretsRawMultiEnv({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
environments,
|
environments: permissiveEnvs,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
projectId,
|
projectId,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
search
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
search,
|
||||||
|
limit: remainingLimit,
|
||||||
|
offset: adjustedOffset,
|
||||||
|
isInternal: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
|
for await (const environment of permissiveEnvs) {
|
||||||
secrets = await server.services.secret.getSecretsRawMultiEnv({
|
const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length;
|
||||||
actorId: req.permission.id,
|
|
||||||
actor: req.permission.type,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
environments,
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
projectId,
|
|
||||||
path: secretPath,
|
|
||||||
orderBy,
|
|
||||||
orderDirection,
|
|
||||||
search,
|
|
||||||
limit: remainingLimit,
|
|
||||||
offset: adjustedOffset
|
|
||||||
});
|
|
||||||
|
|
||||||
for await (const environment of environments) {
|
if (secretCountFromEnv) {
|
||||||
const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length;
|
await server.services.auditLog.createAuditLog({
|
||||||
|
projectId,
|
||||||
|
...req.auditLogInfo,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SECRETS,
|
||||||
|
metadata: {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
numberOfSecrets: secretCountFromEnv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (secretCountFromEnv) {
|
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
projectId,
|
event: PostHogEventTypes.SecretPulled,
|
||||||
...req.auditLogInfo,
|
distinctId: getTelemetryDistinctId(req),
|
||||||
event: {
|
properties: {
|
||||||
type: EventType.GET_SECRETS,
|
numberOfSecrets: secretCountFromEnv,
|
||||||
metadata: {
|
workspaceId: projectId,
|
||||||
environment,
|
environment,
|
||||||
secretPath,
|
secretPath,
|
||||||
numberOfSecrets: secretCountFromEnv
|
channel: getUserAgentType(req.headers["user-agent"]),
|
||||||
}
|
...req.auditLogInfo
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
|
||||||
await server.services.telemetry.sendPostHogEvents({
|
|
||||||
event: PostHogEventTypes.SecretPulled,
|
|
||||||
distinctId: getTelemetryDistinctId(req),
|
|
||||||
properties: {
|
|
||||||
numberOfSecrets: secretCountFromEnv,
|
|
||||||
workspaceId: projectId,
|
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
channel: getUserAgentType(req.headers["user-agent"]),
|
|
||||||
...req.auditLogInfo
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
if (!(error instanceof ForbiddenError)) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -455,31 +455,34 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
const getSecretsCountMultiEnv = async ({
|
const getSecretsCountMultiEnv = async ({
|
||||||
actorId,
|
actorId,
|
||||||
path,
|
path,
|
||||||
|
|
||||||
projectId,
|
projectId,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
environments,
|
environments,
|
||||||
|
isInternal,
|
||||||
...params
|
...params
|
||||||
}: Pick<TGetSecretsDTO, "actorId" | "actor" | "path" | "projectId" | "actorOrgId" | "actorAuthMethod" | "search"> & {
|
}: Pick<TGetSecretsDTO, "actorId" | "actor" | "path" | "projectId" | "actorOrgId" | "actorAuthMethod" | "search"> & {
|
||||||
environments: string[];
|
environments: string[];
|
||||||
|
isInternal?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
if (!isInternal) {
|
||||||
actor,
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actorId,
|
actor,
|
||||||
projectId,
|
actorId,
|
||||||
actorAuthMethod,
|
projectId,
|
||||||
actorOrgId
|
actorAuthMethod,
|
||||||
);
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
// verify user has access to all environments
|
// verify user has access to all environments
|
||||||
environments.forEach((environment) =>
|
environments.forEach((environment) =>
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
|
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
|
||||||
if (!folders.length) return 0;
|
if (!folders.length) return 0;
|
||||||
@@ -546,28 +549,32 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
|
isInternal,
|
||||||
...params
|
...params
|
||||||
}: Pick<TGetSecretsDTO, "actorId" | "actor" | "path" | "projectId" | "actorOrgId" | "actorAuthMethod" | "search"> & {
|
}: Pick<TGetSecretsDTO, "actorId" | "actor" | "path" | "projectId" | "actorOrgId" | "actorAuthMethod" | "search"> & {
|
||||||
environments: string[];
|
environments: string[];
|
||||||
|
isInternal?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
if (!isInternal) {
|
||||||
actor,
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actorId,
|
actor,
|
||||||
projectId,
|
actorId,
|
||||||
actorAuthMethod,
|
projectId,
|
||||||
actorOrgId
|
actorAuthMethod,
|
||||||
);
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
// verify user has access to all environments
|
||||||
|
environments.forEach((environment) =>
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let paths: { folderId: string; path: string; environment: string }[] = [];
|
let paths: { folderId: string; path: string; environment: string }[] = [];
|
||||||
|
|
||||||
// verify user has access to all environments
|
|
||||||
environments.forEach((environment) =>
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
|
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
|
||||||
|
|
||||||
if (!folders.length) {
|
if (!folders.length) {
|
||||||
|
|||||||
@@ -1011,7 +1011,7 @@ export const secretServiceFactory = ({
|
|||||||
}: Pick<
|
}: Pick<
|
||||||
TGetSecretsRawDTO,
|
TGetSecretsRawDTO,
|
||||||
"projectId" | "path" | "actor" | "actorId" | "actorOrgId" | "actorAuthMethod" | "search"
|
"projectId" | "path" | "actor" | "actorId" | "actorOrgId" | "actorAuthMethod" | "search"
|
||||||
> & { environments: string[] }) => {
|
> & { environments: string[]; isInternal?: boolean }) => {
|
||||||
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||||
|
|
||||||
if (!shouldUseSecretV2Bridge)
|
if (!shouldUseSecretV2Bridge)
|
||||||
@@ -1045,6 +1045,7 @@ export const secretServiceFactory = ({
|
|||||||
...params
|
...params
|
||||||
}: Omit<TGetSecretsRawDTO, "environment" | "includeImports" | "expandSecretReferences" | "recursive" | "tagSlugs"> & {
|
}: Omit<TGetSecretsRawDTO, "environment" | "includeImports" | "expandSecretReferences" | "recursive" | "tagSlugs"> & {
|
||||||
environments: string[];
|
environments: string[];
|
||||||
|
isInternal?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,19 @@ export const SecretMainPage = () => {
|
|||||||
});
|
});
|
||||||
const debouncedSearchFilter = useDebounce(filter.searchFilter);
|
const debouncedSearchFilter = useDebounce(filter.searchFilter);
|
||||||
|
|
||||||
|
// change filters if permissions change at different paths/env
|
||||||
|
useEffect(() => {
|
||||||
|
setFilter((prev) => ({
|
||||||
|
...prev,
|
||||||
|
include: {
|
||||||
|
[RowType.Folder]: true,
|
||||||
|
[RowType.Import]: canReadSecret,
|
||||||
|
[RowType.DynamicSecret]: canReadSecret,
|
||||||
|
[RowType.Secret]: canReadSecret
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}, [canReadSecret]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!isWorkspaceLoading &&
|
!isWorkspaceLoading &&
|
||||||
|
|||||||
Reference in New Issue
Block a user