diff --git a/backend/src/ee/services/permission/permission-types.ts b/backend/src/ee/services/permission/permission-types.ts index 8df85054df..1ad0b205b5 100644 --- a/backend/src/ee/services/permission/permission-types.ts +++ b/backend/src/ee/services/permission/permission-types.ts @@ -1,14 +1,7 @@ import picomatch from "picomatch"; import { z } from "zod"; -export enum PermissionConditionOperators { - $IN = "$in", - $ALL = "$all", - $REGEX = "$regex", - $EQ = "$eq", - $NEQ = "$ne", - $GLOB = "$glob" -} +import { PermissionConditionOperators } from "@app/lib/casl"; export const PermissionConditionSchema = { [PermissionConditionOperators.$IN]: z.string().trim().min(1).array(), diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 591cdd343f..c6e574fb12 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -1,10 +1,10 @@ import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability"; import { z } from "zod"; -import { conditionsMatcher } from "@app/lib/casl"; +import { conditionsMatcher, PermissionConditionOperators } from "@app/lib/casl"; import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; -import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types"; +import { PermissionConditionSchema } from "./permission-types"; export enum ProjectPermissionActions { Read = "read", diff --git a/backend/src/lib/casl/index.ts b/backend/src/lib/casl/index.ts index 71625e181c..ad4bf028f3 100644 --- a/backend/src/lib/casl/index.ts +++ b/backend/src/lib/casl/index.ts @@ -54,3 +54,12 @@ export const isAtLeastAsPrivileged = (permissions1: MongoAbility, permissions2: return set1.size >= set2.size; }; + +export enum PermissionConditionOperators { + $IN = "$in", + $ALL = "$all", + $REGEX = "$regex", + $EQ = "$eq", + $NEQ = "$ne", + $GLOB = "$glob" +} diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index 007902a176..2c456aafc9 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -1,4 +1,4 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, PureAbility } from "@casl/ability"; import fastifyPlugin from "fastify-plugin"; import jwt from "jsonwebtoken"; import { ZodError } from "zod"; @@ -63,7 +63,13 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider void res.status(HttpStatusCodes.Forbidden).send({ statusCode: HttpStatusCodes.Forbidden, error: "PermissionDenied", - message: `You are not allowed to ${error.action} on ${error.subjectType} - ${JSON.stringify(error.subject)}` + message: `You are not allowed to ${error.action} on ${error.subjectType}`, + details: (error.ability as PureAbility).rulesFor(error.action as string, error.subjectType).map((el) => ({ + action: el.action, + inverted: el.inverted, + subject: el.subject, + conditions: el.conditions + })) }); } else if (error instanceof ForbiddenRequestError) { void res.status(HttpStatusCodes.Forbidden).send({ diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 87fa2b1200..78575e35d7 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -47,6 +47,7 @@ export const DefaultResponseErrorsSchema = { 403: z.object({ statusCode: z.literal(403), message: z.string(), + details: z.any().optional(), error: z.string() }), 500: z.object({ diff --git a/frontend/src/components/notifications/Notifications.tsx b/frontend/src/components/notifications/Notifications.tsx index befe79e4f3..23b4eebaa6 100644 --- a/frontend/src/components/notifications/Notifications.tsx +++ b/frontend/src/components/notifications/Notifications.tsx @@ -4,13 +4,15 @@ import { Id, toast, ToastContainer, ToastOptions, TypeOptions } from "react-toas export type TNotification = { title?: string; text: ReactNode; + children?: ReactNode; }; -export const NotificationContent = ({ title, text }: TNotification) => { +export const NotificationContent = ({ title, text, children }: TNotification) => { return (
{title &&
{title}
} -
{text}
+
{text}
+ {children &&
{children}
}
); }; @@ -23,7 +25,13 @@ export const createNotification = ( position: "bottom-right", ...toastProps, theme: "dark", - type: myProps?.type || "info", + type: myProps?.type || "info" }); -export const NotificationContainer = () => ; +export const NotificationContainer = () => ( + +); diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 307ef74af3..8f10d5f211 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -33,6 +33,15 @@ export enum PermissionConditionOperators { $GLOB = "$glob" } +export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = { + [PermissionConditionOperators.$EQ]: "equal to", + [PermissionConditionOperators.$IN]: "contains", + [PermissionConditionOperators.$ALL]: "contains all", + [PermissionConditionOperators.$NEQ]: "not equal to", + [PermissionConditionOperators.$GLOB]: "matches glob pattern", + [PermissionConditionOperators.$REGEX]: "matches regex pattern" +}; + export type TPermissionConditionOperators = { [PermissionConditionOperators.$IN]: string[]; [PermissionConditionOperators.$ALL]: string[]; diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index 516a5d7cf3..34120a15d3 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -1,3 +1,4 @@ +import { PureAbility } from "@casl/ability"; import { ZodIssue } from "zod"; export type { TAccessApprovalPolicy } from "./accessApproval/types"; @@ -52,9 +53,14 @@ export type TApiErrors = | { error: ApiErrorTypes.ValidationError; message: ZodIssue[]; + statusCode: 401; + } + | { + error: ApiErrorTypes.ForbiddenError; + message: string; + details: PureAbility["rules"]; statusCode: 403; } - | { error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 401 } | { statusCode: 400; message: string; diff --git a/frontend/src/reactQuery.tsx b/frontend/src/reactQuery.tsx index bf764d2d7a..7b9735c9b1 100644 --- a/frontend/src/reactQuery.tsx +++ b/frontend/src/reactQuery.tsx @@ -3,6 +3,14 @@ import axios from "axios"; import { createNotification } from "@app/components/notifications"; +// akhilmhdh: doing individual imports to avoid cyclic import error +import { Button } from "./components/v2/Button"; +import { Modal, ModalContent, ModalTrigger } from "./components/v2/Modal"; +import { Table, TableContainer, TBody, Td, Th, THead, Tr } from "./components/v2/Table"; +import { + formatedConditionsOperatorNames, + PermissionConditionOperators +} from "./context/ProjectPermissionContext/types"; import { ApiErrorTypes, TApiErrors } from "./hooks/api/types"; // this is saved in react-query cache @@ -10,35 +18,151 @@ export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"]; export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"]; export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"]; +const camelCaseToSpaces = (input: string) => { + return input.replace(/([a-z])([A-Z])/g, "$1 $2"); +}; + export const queryClient = new QueryClient({ mutationCache: new MutationCache({ onError: (error) => { if (axios.isAxiosError(error)) { const serverResponse = error.response?.data as TApiErrors; if (serverResponse?.error === ApiErrorTypes.ValidationError) { - createNotification({ - title: "Validation Error", - type: "error", - text: ( -
- {serverResponse.message?.map(({ message, path }) => ( -
-
- Field {path.join(".")} {message.toLowerCase()} -
-
- ))} -
- ) - }); + createNotification( + { + title: "Validation Error", + type: "error", + text: "Please check the input and try again.", + children: ( + + + + + + + + + + + + + + + {serverResponse.message?.map(({ message, path }) => ( + + + + + ))} + +
FieldIssue
{path.join(".")}{message.toLowerCase()}
+
+
+
+ ) + }, + { closeOnClick: false } + ); return; } - if (serverResponse.statusCode === 401) { - createNotification({ - title: "Forbidden Access", - type: "error", - text: serverResponse.message - }); + if (serverResponse?.error === ApiErrorTypes.ForbiddenError) { + createNotification( + { + title: "Forbidden Access", + type: "error", + text: serverResponse.message, + children: serverResponse?.details?.length ? ( + + + + + +
+ {serverResponse.details?.map((el, index) => { + const hasConditions = Object.keys(el.conditions || {}).length; + return ( +
+
+ {el.inverted ? "Cannot" : "Can"}{" "} + + {el.action.toString().replaceAll(",", ", ")} + {" "} + {el.subject.toString()} {hasConditions && "with conditions:"} +
+ {hasConditions && ( +
    + {Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => { + const operators = ( + el.conditions as Record< + string, + | string + | { [K in PermissionConditionOperators]: string | string[] } + > + )[field]; + + const formattedFieldName = camelCaseToSpaces(field).toLowerCase(); + if (typeof operators === "string") { + return ( +
  • + + {formattedFieldName} + {" "} + equal to{" "} + {operators} +
  • + ); + } + + return Object.keys(operators).map((operator, operatorIndex) => ( +
  • + + {formattedFieldName} + {" "} + + { + formatedConditionsOperatorNames[ + operator as PermissionConditionOperators + ] + } + {" "} + + {operators[ + operator as PermissionConditionOperators + ].toString()} + +
  • + )); + })} +
+ )} +
+ ); + })} +
+
+
+ ) : undefined + }, + { closeOnClick: false } + ); return; } createNotification({ title: "Bad Request", type: "error", text: serverResponse.message }); diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index 0ccfbc4667..916244a5a0 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -13,6 +13,14 @@ html { @apply rounded-md; } +.Toastify__toast-body { + @apply items-start; +} + +.Toastify__toast-icon { + @apply w-4 pt-1; +} + .rdp-day, .rdp-nav_button { @apply rounded-md hover:text-mineshaft-500; diff --git a/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx index 06abc08e61..09a905b2d9 100644 --- a/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -154,7 +154,7 @@ export const CreateSecretForm = ({ isMulti name="tagIds" isDisabled={!canReadTags} - isLoading={isTagsLoading} + isLoading={isTagsLoading && canReadTags} options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))} value={field.value} onChange={field.onChange} diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx index ec5b064c1e..be9ee77ad3 100644 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx @@ -867,7 +867,7 @@ export const SecretOverviewPage = () => {
setScrollOffset(e.currentTarget.scrollLeft)} - className="thin-scrollbar" + className="thin-scrollbar rounded-b-none" > diff --git a/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx index 0fd953a1fa..91a6e766f1 100644 --- a/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -255,7 +255,7 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }: isMulti name="tagIds" isDisabled={!canReadTags} - isLoading={isTagsLoading} + isLoading={isTagsLoading && canReadTags} options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))} value={field.value} onChange={field.onChange}