From 7317dc1cf5ff0eb7809d0175a73a9c19d4d3e815 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 20 Nov 2024 22:50:21 +0530 Subject: [PATCH 1/5] feat: modified error handler to return possible rules for a validation failed rules --- backend/src/ee/services/permission/permission-types.ts | 9 +-------- .../src/ee/services/permission/project-permission.ts | 4 ++-- backend/src/lib/casl/index.ts | 9 +++++++++ backend/src/server/plugins/error-handler.ts | 10 ++++++++-- backend/src/server/routes/sanitizedSchemas.ts | 1 + 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/backend/src/ee/services/permission/permission-types.ts b/backend/src/ee/services/permission/permission-types.ts index 8df85054d..1ad0b205b 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 591cdd343..c6e574fb1 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 71625e181..ad4bf028f 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 007902a17..2c456aafc 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 87fa2b120..78575e35d 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({ From 863719f2963bfb9548b375f8f653722d83fa712f Mon Sep 17 00:00:00 2001 From: = Date: Wed, 20 Nov 2024 22:55:14 +0530 Subject: [PATCH 2/5] feat: added action button for notification toast and one action each for forbidden error and validation error details --- .../notifications/Notifications.tsx | 16 +- .../context/ProjectPermissionContext/types.ts | 9 + frontend/src/hooks/api/types.ts | 8 +- frontend/src/reactQuery.tsx | 166 +++++++++++++++--- frontend/src/styles/globals.css | 8 + 5 files changed, 181 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/notifications/Notifications.tsx b/frontend/src/components/notifications/Notifications.tsx index befe79e4f..23b4eebaa 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 307ef74af..818a5d0e6 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", + [PermissionConditionOperators.$IN]: "containing", + [PermissionConditionOperators.$ALL]: "contains all", + [PermissionConditionOperators.$NEQ]: "not equal", + [PermissionConditionOperators.$GLOB]: "glob matching", + [PermissionConditionOperators.$REGEX]: "regex" +}; + 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 516a5d7cf..34120a15d 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 bf764d2d7..5083f9db3 100644 --- a/frontend/src/reactQuery.tsx +++ b/frontend/src/reactQuery.tsx @@ -3,6 +3,23 @@ import axios from "axios"; import { createNotification } from "@app/components/notifications"; +import { + Button, + Modal, + ModalContent, + ModalTrigger, + Table, + TableContainer, + TBody, + Td, + Th, + THead, + Tr +} from "./components/v2"; +import { + formatedConditionsOperatorNames, + PermissionConditionOperators +} from "./context/ProjectPermissionContext/types"; import { ApiErrorTypes, TApiErrors } from "./hooks/api/types"; // this is saved in react-query cache @@ -10,35 +27,142 @@ 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()}{" "} + {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{" "} + {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 0ccfbc466..916244a5a 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; From 300372fa980e4cdb26f42689d82e3d35549ebcf2 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 20 Nov 2024 23:59:49 +0530 Subject: [PATCH 3/5] feat: resolve dependency cycle error --- frontend/src/reactQuery.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/frontend/src/reactQuery.tsx b/frontend/src/reactQuery.tsx index 5083f9db3..0ce8325b1 100644 --- a/frontend/src/reactQuery.tsx +++ b/frontend/src/reactQuery.tsx @@ -3,19 +3,10 @@ import axios from "axios"; import { createNotification } from "@app/components/notifications"; -import { - Button, - Modal, - ModalContent, - ModalTrigger, - Table, - TableContainer, - TBody, - Td, - Th, - THead, - Tr -} from "./components/v2"; +// 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 e5d4677fd67293244364ccfbbcb8039ca34add0f Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Wed, 20 Nov 2024 11:50:10 -0800 Subject: [PATCH 4/5] improvements: minor UI/labeling adjustments, only show tags loading if can read, and remove rounded bottom on overview table --- .../src/ee/services/license/license-fns.ts | 4 +- .../context/ProjectPermissionContext/types.ts | 10 ++--- frontend/src/reactQuery.tsx | 43 +++++++++++-------- .../CreateSecretForm/CreateSecretForm.tsx | 2 +- .../SecretOverviewPage/SecretOverviewPage.tsx | 2 +- .../CreateSecretForm/CreateSecretForm.tsx | 2 +- 6 files changed, 36 insertions(+), 27 deletions(-) diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 70c299564..872f38fec 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -17,11 +17,11 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ environmentsUsed: 0, identityLimit: null, identitiesUsed: 0, - dynamicSecret: false, + dynamicSecret: true, secretVersioning: true, pitRecovery: false, ipAllowlisting: false, - rbac: false, + rbac: true, customRateLimits: false, customAlerts: false, auditLogs: false, diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 818a5d0e6..8f10d5f21 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -34,12 +34,12 @@ export enum PermissionConditionOperators { } export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = { - [PermissionConditionOperators.$EQ]: "equal", - [PermissionConditionOperators.$IN]: "containing", + [PermissionConditionOperators.$EQ]: "equal to", + [PermissionConditionOperators.$IN]: "contains", [PermissionConditionOperators.$ALL]: "contains all", - [PermissionConditionOperators.$NEQ]: "not equal", - [PermissionConditionOperators.$GLOB]: "glob matching", - [PermissionConditionOperators.$REGEX]: "regex" + [PermissionConditionOperators.$NEQ]: "not equal to", + [PermissionConditionOperators.$GLOB]: "matches glob pattern", + [PermissionConditionOperators.$REGEX]: "matches regex pattern" }; export type TPermissionConditionOperators = { diff --git a/frontend/src/reactQuery.tsx b/frontend/src/reactQuery.tsx index 0ce8325b1..7b9735c9b 100644 --- a/frontend/src/reactQuery.tsx +++ b/frontend/src/reactQuery.tsx @@ -94,11 +94,13 @@ export const queryClient = new QueryClient({ >
{el.inverted ? "Cannot" : "Can"}{" "} - {el.action.toString()}{" "} - {el.subject.toString()} {hasConditions && "with conditions"} + + {el.action.toString().replaceAll(",", ", ")} + {" "} + {el.subject.toString()} {hasConditions && "with conditions:"}
{hasConditions && ( -
+
    {Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => { const operators = ( el.conditions as Record< @@ -111,38 +113,45 @@ export const queryClient = new QueryClient({ const formattedFieldName = camelCaseToSpaces(field).toLowerCase(); if (typeof operators === "string") { return ( -
    - {formattedFieldName} equal{" "} - {operators} -
    + + {formattedFieldName} + {" "} + equal to{" "} + {operators} + ); } return Object.keys(operators).map((operator, operatorIndex) => ( -
    - {formattedFieldName}{" "} - { - formatedConditionsOperatorNames[ - operator as PermissionConditionOperators - ] - }{" "} - + + {formattedFieldName} + {" "} + + { + formatedConditionsOperatorNames[ + operator as PermissionConditionOperators + ] + } + {" "} + {operators[ operator as PermissionConditionOperators ].toString()} -
    + )); })} -
+ )} ); diff --git a/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx index 06abc08e6..09a905b2d 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 ec5b064c1..be9ee77ad 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 0fd953a1f..91a6e766f 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} From 1eda7aaaac8817004c03d693cf026c597824a809 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Wed, 20 Nov 2024 12:14:14 -0800 Subject: [PATCH 5/5] reverse license --- backend/src/ee/services/license/license-fns.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 872f38fec..70c299564 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -17,11 +17,11 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ environmentsUsed: 0, identityLimit: null, identitiesUsed: 0, - dynamicSecret: true, + dynamicSecret: false, secretVersioning: true, pitRecovery: false, ipAllowlisting: false, - rbac: true, + rbac: false, customRateLimits: false, customAlerts: false, auditLogs: false,