fix: refactor secrets overview endpoint to filter envs for secrets with read permissions

This commit is contained in:
Scott Wilson
2024-09-26 09:24:29 -07:00
parent 592cc13b1f
commit cd71a13bb7
6 changed files with 192 additions and 143 deletions
@@ -313,8 +313,10 @@ export const dynamicSecretServiceFactory = ({
projectId, projectId,
path, path,
environmentSlugs, environmentSlugs,
search search,
isInternal
}: TListDynamicSecretsMultiEnvDTO) => { }: TListDynamicSecretsMultiEnvDTO) => {
if (!isInternal) {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -330,6 +332,7 @@ export const dynamicSecretServiceFactory = ({
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,8 +437,10 @@ export const dynamicSecretServiceFactory = ({
path, path,
environmentSlugs, environmentSlugs,
projectId, projectId,
isInternal,
...params ...params
}: TListDynamicSecretsMultiEnvDTO) => { }: TListDynamicSecretsMultiEnvDTO) => {
if (!isInternal) {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -451,6 +456,7 @@ export const dynamicSecretServiceFactory = ({
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,7 +175,29 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
} }
} }
try { if (!includeDynamicSecrets && !includeSecrets)
return {
folders,
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) { if (includeDynamicSecrets) {
// 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
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({ totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
@@ -184,8 +207,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId, projectId,
search, search,
environmentSlugs: environments, environmentSlugs: permissiveEnvs,
path: secretPath path: secretPath,
isInternal: true
}); });
if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) { if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) {
@@ -198,10 +222,11 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
search, search,
orderBy, orderBy,
orderDirection, orderDirection,
environmentSlugs: environments, environmentSlugs: permissiveEnvs,
path: secretPath, path: secretPath,
limit: remainingLimit, limit: remainingLimit,
offset: adjustedOffset offset: adjustedOffset,
isInternal: true
}); });
// get the count of unique dynamic secret names to properly adjust remaining limit // get the count of unique dynamic secret names to properly adjust remaining limit
@@ -220,11 +245,12 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
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 search,
isInternal: true
}); });
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
@@ -232,7 +258,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
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,
@@ -240,10 +266,11 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
orderDirection, orderDirection,
search, search,
limit: remainingLimit, limit: remainingLimit,
offset: adjustedOffset offset: adjustedOffset,
isInternal: true
}); });
for await (const environment of environments) { for await (const environment of permissiveEnvs) {
const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length; const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length;
if (secretCountFromEnv) { if (secretCountFromEnv) {
@@ -278,11 +305,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
} }
} }
} }
} catch (error) {
if (!(error instanceof ForbiddenError)) {
throw error;
}
}
return { return {
folders, folders,
@@ -455,16 +455,18 @@ 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;
}) => { }) => {
if (!isInternal) {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -480,6 +482,7 @@ export const secretV2BridgeServiceFactory = ({
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,10 +549,13 @@ 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;
}) => { }) => {
if (!isInternal) {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -558,8 +564,6 @@ export const secretV2BridgeServiceFactory = ({
actorOrgId actorOrgId
); );
let paths: { folderId: string; path: string; environment: string }[] = [];
// 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(
@@ -567,6 +571,9 @@ export const secretV2BridgeServiceFactory = ({
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
) )
); );
}
let paths: { folderId: string; path: string; environment: string }[] = [];
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
@@ -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 &&