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: (
+
+
+
+
+
+
+
+
+
+ Field |
+ Issue |
+
+
+
+ {serverResponse.message?.map(({ message, path }) => (
+
+ {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}