Merge remote-tracking branch 'origin/main' into misc/privilege-management-v2-transition

This commit is contained in:
Sheen Capadngan
2025-03-10 15:35:48 +08:00
41 changed files with 1625 additions and 458 deletions
@@ -14,7 +14,6 @@ import {
AccordionItem,
AccordionTrigger,
Button,
Checkbox,
FormControl,
Input,
Modal,
@@ -33,13 +32,7 @@ import {
useUser
} from "@app/context";
import { getProjectHomePage } from "@app/helpers/project";
import {
fetchOrgUsers,
useAddUserToWsNonE2EE,
useCreateWorkspace,
useGetExternalKmsList,
useGetUserWorkspaces
} from "@app/hooks/api";
import { useCreateWorkspace, useGetExternalKmsList, useGetUserWorkspaces } from "@app/hooks/api";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
import { ProjectType } from "@app/hooks/api/workspace/types";
@@ -51,7 +44,6 @@ const formSchema = z.object({
.trim()
.max(256, "Description too long, max length is 256 characters")
.optional(),
addMembers: z.boolean(),
kmsKeyId: z.string(),
template: z.string()
});
@@ -73,7 +65,6 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
const { user } = useUser();
const createWs = useCreateWorkspace();
const { refetch: refetchWorkspaces } = useGetUserWorkspaces();
const addUsersToProject = useAddUserToWsNonE2EE();
const { subscription } = useSubscription();
const canReadProjectTemplates = permission.can(
@@ -111,7 +102,6 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
const onCreateProject = async ({
name,
description,
addMembers,
kmsKeyId,
template
}: TAddProjectFormData) => {
@@ -128,21 +118,6 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
template,
type: projectType
});
const { id: newProjectId } = project;
if (addMembers) {
const orgUsers = await fetchOrgUsers(currentOrg.id);
await addUsersToProject.mutateAsync({
usernames: orgUsers
.filter(
(member) => member.user.username !== user.username && member.status === "accepted"
)
.map((member) => member.user.username),
projectId: newProjectId,
orgId: currentOrg.id
});
}
await refetchWorkspaces();
createNotification({ text: "Project created", type: "success" });
@@ -246,31 +221,7 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
)}
/>
</div>
<div className="mt-4 pl-1">
<Controller
control={control}
name="addMembers"
defaultValue={false}
render={({ field: { onBlur, value, onChange } }) => (
<OrgPermissionCan I={OrgPermissionActions.Read} a={OrgPermissionSubjects.Member}>
{(isAllowed) => (
<div>
<Checkbox
id="add-project-layout"
isChecked={value}
onCheckedChange={onChange}
isDisabled={!isAllowed}
onBlur={onBlur}
>
Add all members of my organization to this project
</Checkbox>
</div>
)}
</OrgPermissionCan>
)}
/>
</div>
<div className="mt-14 flex">
<div className="mt-4 flex">
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="advance-settings" className="data-[state=open]:border-none">
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
+101
View File
@@ -74,6 +74,107 @@ export const queryClient = new QueryClient({
);
return;
}
if (serverResponse?.error === ApiErrorTypes.PermissionBoundaryError) {
createNotification(
{
title: "Forbidden Access",
type: "error",
text: `${serverResponse.message}.`,
callToAction: serverResponse?.details?.missingPermissions?.length ? (
<Modal>
<ModalTrigger asChild>
<Button variant="outline_bg" size="xs">
Show more
</Button>
</ModalTrigger>
<ModalContent title="Missing Permission">
<div className="flex flex-col gap-2">
{serverResponse.details?.missingPermissions?.map((el, index) => {
const hasConditions = Boolean(Object.keys(el.conditions || {}).length);
return (
<div
key={`Forbidden-error-details-${index + 1}`}
className="rounded-md border border-gray-600 p-4"
>
<div>
You are not authorized to perform the <b>{el.action}</b> action on the{" "}
<b>{el.subject}</b> resource.{" "}
{hasConditions &&
"Your permission does not allow access to the following conditions:"}
</div>
{hasConditions && (
<ul className="flex list-disc flex-col gap-1 pl-5 pt-2 text-sm">
{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 (
<li
key={`Forbidden-error-details-${index + 1}-${
fieldIndex + 1
}`}
>
<span className="font-bold capitalize">
{formattedFieldName}
</span>{" "}
<span className="text-mineshaft-200">equal to</span>{" "}
<span className="text-yellow-600">{operators}</span>
</li>
);
}
return Object.keys(operators).map((operator, operatorIndex) => (
<li
key={`Forbidden-error-details-${index + 1}-${
fieldIndex + 1
}-${operatorIndex + 1}`}
>
<span className="font-bold capitalize">
{formattedFieldName}
</span>{" "}
<span className="text-mineshaft-200">
{
formatedConditionsOperatorNames[
operator as PermissionConditionOperators
]
}
</span>{" "}
<span className="text-yellow-600">
{operators[
operator as PermissionConditionOperators
].toString()}
</span>
</li>
));
})}
</ul>
)}
</div>
);
})}
</div>
</ModalContent>
</Modal>
) : undefined,
copyActions: [
{
value: serverResponse.reqId,
name: "Request ID",
label: `Request ID: ${serverResponse.reqId}`
}
]
},
{ closeOnClick: false }
);
return;
}
if (serverResponse?.error === ApiErrorTypes.ForbiddenError) {
createNotification(
{
+14
View File
@@ -44,6 +44,7 @@ export type {
export enum ApiErrorTypes {
ValidationError = "ValidationFailure",
PermissionBoundaryError = "PermissionBoundaryError",
BadRequestError = "BadRequest",
UnauthorizedError = "UnauthorizedError",
ForbiddenError = "PermissionDenied"
@@ -74,4 +75,17 @@ export type TApiErrors =
statusCode: 400;
message: string;
error: ApiErrorTypes.BadRequestError;
}
| {
reqId: string;
statusCode: 403;
message: string;
error: ApiErrorTypes.PermissionBoundaryError;
details: {
missingPermissions: {
action: string;
subject: string;
conditions: Record<string, Record<string, string>>;
}[];
};
};
@@ -36,7 +36,7 @@ const formSchema = z.object({
revocationStatement: z.string().min(1),
renewStatement: z.string().optional(),
ca: z.string().optional(),
projectGatewayId: z.string().optional()
projectGatewayId: z.string().optional().nullable()
})
.partial(),
defaultTTL: z.string().superRefine((val, ctx) => {
@@ -207,7 +207,7 @@ export const EditDynamicSecretSqlProviderForm = ({
helperText=""
>
<Select
value={value}
value={value || undefined}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
@@ -463,7 +463,7 @@ export const SecretDetailSidebar = ({
<div
className={`grid auto-cols-min grid-flow-col gap-2 overflow-hidden ${tagFields.fields.length > 0 ? "pt-2" : ""}`}
>
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
{tagFields.fields.map(({ tagColor, id: formId, slug }) => (
<Tag
className="flex w-min items-center space-x-2"
key={formId}
@@ -472,7 +472,8 @@ export const SecretDetailSidebar = ({
createNotification({ type: "error", text: "Access denied" });
return;
}
const tag = tags?.find(({ id: tagId }) => id === tagId);
const tag = tags?.find(({ slug: tagSlug }) => slug === tagSlug);
if (tag) handleTagSelect(tag);
}}
>
@@ -241,8 +241,8 @@ export const SecretListView = ({
let successMessage;
if (isReminderEvent) {
successMessage = reminderRepeatDays
? "Successfully saved secret reminder"
successMessage = reminderRepeatDays
? "Successfully saved secret reminder"
: "Successfully deleted secret reminder";
} else {
successMessage = "Successfully saved secrets";