mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 01:38:05 +00:00
Merge branch 'main' into feat/point-in-time-revamp
This commit is contained in:
@@ -17,7 +17,9 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
import { DuplicateProjectRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal";
|
||||
import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
|
||||
import { RoleDetailsSection } from "./components/RoleDetailsSection";
|
||||
@@ -39,7 +41,8 @@ const Page = () => {
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"role",
|
||||
"deleteRole"
|
||||
"deleteRole",
|
||||
"duplicateRole"
|
||||
] as const);
|
||||
|
||||
const onDeleteRoleSubmit = async () => {
|
||||
@@ -77,7 +80,9 @@ const Page = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(data?.slug ?? "");
|
||||
const isCustomRole = !Object.values(ProjectMembershipRole).includes(
|
||||
(data?.slug ?? "") as ProjectMembershipRole
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
@@ -114,6 +119,24 @@ const Page = () => {
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("duplicateRole");
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Duplicate Role
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Role}
|
||||
@@ -152,6 +175,11 @@ const Page = () => {
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() => onDeleteRoleSubmit()}
|
||||
/>
|
||||
<DuplicateProjectRoleModal
|
||||
isOpen={popUp.duplicateRole.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("duplicateRole", isOpen)}
|
||||
roleSlug={roleSlug}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { faAngleDown, faLayerGroup, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
IconButton
|
||||
} from "@app/components/v2";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
|
||||
import { PolicyTemplateModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal";
|
||||
|
||||
type Props = {
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const AddPoliciesButton = ({ isDisabled }: Props) => {
|
||||
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"addPolicy",
|
||||
"addPolicyOptions",
|
||||
"applyTemplate"
|
||||
] as const);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className="h-10 rounded-r-none"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={isDisabled}
|
||||
onClick={() => handlePopUpToggle("addPolicy")}
|
||||
>
|
||||
Add Policies
|
||||
</Button>
|
||||
<DropdownMenu
|
||||
open={popUp.addPolicyOptions.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addPolicyOptions", isOpen)}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Open policy template options"
|
||||
variant="outline_bg"
|
||||
className="rounded-l-none bg-mineshaft-600 p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleDown} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<div className="flex flex-col space-y-1 p-1.5">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faLayerGroup} className="pr-2" />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("applyTemplate");
|
||||
handlePopUpClose("addPolicyOptions");
|
||||
}}
|
||||
isDisabled={isDisabled}
|
||||
variant="outline_bg"
|
||||
className="h-10 text-left"
|
||||
isFullWidth
|
||||
>
|
||||
Add From Template
|
||||
</Button>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<PolicySelectionModal
|
||||
isOpen={popUp.addPolicy.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
|
||||
/>
|
||||
<PolicyTemplateModal
|
||||
isOpen={popUp.applyTemplate.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("applyTemplate", isOpen)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Modal, ModalContent, Spinner } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useCreateProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api";
|
||||
import { TProjectRole } from "@app/hooks/api/roles/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
roleSlug?: string;
|
||||
};
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
name: z.string().min(1, "Name required"),
|
||||
description: z.string(),
|
||||
slug: slugSchema({ min: 1 })
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type ContentProps = {
|
||||
role: TProjectRole;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const Content = ({ role, onClose }: ContentProps) => {
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
defaultValues: {
|
||||
name: `${role.name} Duplicate`
|
||||
},
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const createRole = useCreateProjectRole();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleDuplicateRole = async (form: FormData) => {
|
||||
const newRole = await createRole.mutateAsync({
|
||||
projectId: currentWorkspace.id,
|
||||
permissions: role.permissions,
|
||||
...form
|
||||
});
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Role duplicated successfully"
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: `/${currentWorkspace.type}/$projectId/roles/$roleSlug` as const,
|
||||
params: {
|
||||
roleSlug: newRole.slug,
|
||||
projectId: currentWorkspace.id
|
||||
}
|
||||
});
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleDuplicateRole)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message} isRequired>
|
||||
<Input {...field} placeholder="Billing Team" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="slug"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Slug" isError={Boolean(error)} errorText={error?.message} isRequired>
|
||||
<Input {...field} placeholder="billing" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="description"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Description" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="To manage billing" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Duplicate Role
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const DuplicateProjectRoleModal = ({ isOpen, onOpenChange, roleSlug }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { data: role, isPending } = useGetProjectRoleBySlug(currentWorkspace.id, roleSlug ?? "");
|
||||
|
||||
if (!roleSlug) return null;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Duplicate Role"
|
||||
subTitle="Duplicate this role to create a new role with the same permissions."
|
||||
>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{isPending ? (
|
||||
<div className="flex h-full flex-col items-center justify-center py-2.5">
|
||||
<Spinner size="lg" className="text-mineshaft-500" />
|
||||
<p className="mt-4 text-sm text-mineshaft-400">Loading Role...</p>
|
||||
</div>
|
||||
) : role ? (
|
||||
<Content role={role!} onClose={() => onOpenChange(false)} />
|
||||
) : (
|
||||
<p className="w-full text-center text-red">
|
||||
Error: could not find role with slug "{roleSlug}"
|
||||
</p>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+55
-22
@@ -1,5 +1,5 @@
|
||||
import { cloneElement, useState } from "react";
|
||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { cloneElement, ReactNode, useState } from "react";
|
||||
import { Control, Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form";
|
||||
import {
|
||||
faChevronDown,
|
||||
faChevronRight,
|
||||
@@ -29,6 +29,43 @@ type Props<T extends ProjectPermissionSub> = {
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
type ActionProps = {
|
||||
value: string;
|
||||
subject: ProjectPermissionSub;
|
||||
rootIndex: number;
|
||||
label: ReactNode;
|
||||
isDisabled?: boolean;
|
||||
control: Control<TFormSchema>;
|
||||
};
|
||||
|
||||
const ActionCheckbox = ({ value, subject, isDisabled, rootIndex, label, control }: ActionProps) => {
|
||||
// scott: using Controller caused discrepancy between field value and actual value, this is a hacky fix
|
||||
const fieldValue = useWatch({
|
||||
control,
|
||||
name: `permissions.${subject}.${rootIndex}.${value}` as any
|
||||
});
|
||||
const { setValue } = useFormContext();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
isDisabled={isDisabled}
|
||||
isChecked={Boolean(fieldValue)}
|
||||
onCheckedChange={(isChecked) =>
|
||||
setValue(`permissions.${subject}.${rootIndex}.${value}`, isChecked, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: true
|
||||
})
|
||||
}
|
||||
id={`permissions.${subject}.${rootIndex}.${String(value)}`}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchema["permissions"]>>({
|
||||
subject,
|
||||
actions,
|
||||
@@ -41,11 +78,18 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
||||
control,
|
||||
name: `permissions.${subject}`
|
||||
});
|
||||
|
||||
// scott: this is a hacky work-around to resolve bug of fields not updating UI when removed
|
||||
const watchFields = useWatch<TFormSchema>({
|
||||
control,
|
||||
name: `permissions.${subject}`
|
||||
});
|
||||
|
||||
const [isOpen, setIsOpen] = useToggle();
|
||||
const [draggedItem, setDraggedItem] = useState<number | null>(null);
|
||||
const [dragOverItem, setDragOverItem] = useState<number | null>(null);
|
||||
|
||||
if (!fields.length) return <div />;
|
||||
if (!watchFields || !Array.isArray(watchFields) || watchFields.length === 0) return <div />;
|
||||
|
||||
const handleDragStart = (_: React.DragEvent, index: number) => {
|
||||
setDraggedItem(index);
|
||||
@@ -114,7 +158,7 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
||||
<div
|
||||
key={el.id}
|
||||
className={twMerge(
|
||||
"relative bg-mineshaft-800 p-5 first:rounded-t-md last:rounded-b-md",
|
||||
"relative bg-mineshaft-800 p-5 pr-10 first:rounded-t-md last:rounded-b-md",
|
||||
dragOverItem === rootIndex ? "border-2 border-blue-400" : "",
|
||||
draggedItem === rootIndex ? "opacity-50" : ""
|
||||
)}
|
||||
@@ -179,7 +223,7 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex text-gray-300">
|
||||
<div className="flex gap-4 text-gray-300">
|
||||
<div className="w-1/4">Actions</div>
|
||||
<div className="flex flex-grow flex-wrap justify-start gap-8">
|
||||
{actions.map(({ label, value }, index) => {
|
||||
@@ -194,25 +238,14 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
||||
}
|
||||
|
||||
return (
|
||||
<Controller
|
||||
<ActionCheckbox
|
||||
key={`${el.id}-${index + 1}`}
|
||||
name={`permissions.${subject}.${rootIndex}.${value}` as any}
|
||||
value={value}
|
||||
label={label}
|
||||
rootIndex={rootIndex}
|
||||
control={control}
|
||||
defaultValue={false}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
isDisabled={isDisabled}
|
||||
isChecked={Boolean(field.value)}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`permissions.${subject}.${rootIndex}.${String(value)}`}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
subject={subject}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
ModalClose,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
|
||||
import {
|
||||
isConditionalSubjects,
|
||||
PROJECT_PERMISSION_OBJECT,
|
||||
projectRoleFormSchema,
|
||||
TFormSchema
|
||||
} from "./ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const NewPermissionRule = ({ onClose }: Props) => {
|
||||
const rootForm = useFormContext<TFormSchema>();
|
||||
|
||||
const form = useForm<{
|
||||
type: ProjectPermissionSub;
|
||||
permissions: NonNullable<TFormSchema["permissions"]>;
|
||||
}>({
|
||||
resolver: zodResolver(
|
||||
projectRoleFormSchema
|
||||
.pick({ permissions: true })
|
||||
.extend({ type: z.nativeEnum(ProjectPermissionSub) })
|
||||
),
|
||||
defaultValues: {
|
||||
type: ProjectPermissionSub.Secrets
|
||||
}
|
||||
});
|
||||
|
||||
const selectedSubject = form.watch("type");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="type"
|
||||
defaultValue={ProjectPermissionSub.Secrets}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Subject" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{Object.keys(PROJECT_PERMISSION_OBJECT).map((subject) => (
|
||||
<SelectItem value={subject} key={`permission-create-${subject}`}>
|
||||
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormLabel label="Actions" className="my-2" />
|
||||
<div className="flex flex-grow flex-wrap justify-start gap-8">
|
||||
{PROJECT_PERMISSION_OBJECT?.[selectedSubject]?.actions?.map(({ label, value }) => (
|
||||
<Controller
|
||||
key={`create-permission-${selectedSubject}-${label}`}
|
||||
name={`permissions.${selectedSubject}.0.${value as any}` as any}
|
||||
control={form.control}
|
||||
defaultValue={false}
|
||||
render={({ field }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`new-permissions.${selectedSubject}.0.${String(value)}`}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8 flex space-x-4">
|
||||
<Button
|
||||
onClick={form.handleSubmit((el) => {
|
||||
const rootPolicyValue = rootForm.getValues("permissions")?.[el.type];
|
||||
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
|
||||
rootForm.setValue(
|
||||
`permissions.${el.type}`,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
[...rootPolicyValue, ...(el?.permissions[el.type] || [])],
|
||||
{ shouldDirty: true, shouldTouch: true }
|
||||
);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
rootForm.setValue(`permissions.${el.type}`, el?.permissions?.[el.type], {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
})}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
PermissionConditionOperators,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
|
||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
position?: number;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const PkiSubscriberPermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
formState: { errors }
|
||||
} = useFormContext<TFormSchema>();
|
||||
|
||||
const permissionSubject = ProjectPermissionSub.PkiSubscribers;
|
||||
const items = useFieldArray({
|
||||
control,
|
||||
name: `permissions.${permissionSubject}.${position}.conditions`
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
||||
<p className="mt-2 text-gray-300">Conditions</p>
|
||||
<p className="text-sm text-mineshaft-400">
|
||||
Conditions determine when a policy will be applied (always if no conditions are present).
|
||||
</p>
|
||||
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
|
||||
All conditions must evaluate to true for the policy to take effect.
|
||||
</p>
|
||||
<div className="mt-2 flex flex-col space-y-2">
|
||||
{items.fields.map((el, index) => {
|
||||
const condition =
|
||||
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
|
||||
lhs: string;
|
||||
rhs: string;
|
||||
operator: string;
|
||||
}) || {};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={el.id}
|
||||
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
|
||||
>
|
||||
<div className="w-1/4">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${permissionSubject}.${position}.conditions.${index}.lhs`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => field.onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value="name">Name</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-36 items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${permissionSubject}.${position}.conditions.${index}.operator`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => field.onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value={PermissionConditionOperators.$EQ}>Equals</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Tooltip
|
||||
asChild
|
||||
content={getConditionOperatorHelperInfo(
|
||||
condition?.operator as PermissionConditionOperators
|
||||
)}
|
||||
className="max-w-xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
ariaLabel="plus"
|
||||
variant="outline_bg"
|
||||
className="p-2.5"
|
||||
onClick={() => items.remove(index)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
|
||||
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
|
||||
<FontAwesomeIcon icon={faWarning} className="text-red" />
|
||||
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
variant="star"
|
||||
size="xs"
|
||||
className="mt-3"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() =>
|
||||
items.append({
|
||||
lhs: "name",
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
rhs: ""
|
||||
})
|
||||
}
|
||||
>
|
||||
Add Condition
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,214 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||
import { faCheck, faSearch, faXmark, faXmarkCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { useGetProjectTypeFromRoute } from "@app/hooks";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import {
|
||||
isConditionalSubjects,
|
||||
PROJECT_PERMISSION_OBJECT,
|
||||
ProjectTypePermissionSubjects,
|
||||
TFormSchema
|
||||
} from "./ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
type TForm = { permissions: Record<ProjectPermissionSub, boolean> };
|
||||
|
||||
const Content = ({ onClose }: ContentProps) => {
|
||||
const rootForm = useFormContext<TFormSchema>();
|
||||
const [search, setSearch] = useState("");
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isDirty },
|
||||
setValue,
|
||||
reset
|
||||
} = useForm<TForm>({
|
||||
defaultValues: {
|
||||
permissions: Object.fromEntries(
|
||||
Object.values(ProjectPermissionSub).map((subject) => [subject, false])
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
const projectType = useGetProjectTypeFromRoute();
|
||||
|
||||
const filteredPolicies = Object.entries(PROJECT_PERMISSION_OBJECT)
|
||||
.filter(
|
||||
([subject, { title }]) =>
|
||||
ProjectTypePermissionSubjects[projectType ?? ProjectType.SecretManager][
|
||||
subject as ProjectPermissionSub
|
||||
] && (search ? title.toLowerCase().includes(search.toLowerCase()) : true)
|
||||
)
|
||||
.sort((a, b) => a[1].title.localeCompare(b[1].title))
|
||||
.map(([subject]) => subject);
|
||||
|
||||
const onSubmit = () =>
|
||||
handleSubmit((form) => {
|
||||
Object.entries(form.permissions).forEach(([subject, add]) => {
|
||||
if (!add) return;
|
||||
|
||||
const type = subject as ProjectPermissionSub;
|
||||
|
||||
const rootPolicyValue = rootForm.getValues("permissions")?.[type];
|
||||
|
||||
if (rootPolicyValue && isConditionalSubjects(subject as ProjectPermissionSub)) {
|
||||
rootForm.setValue(
|
||||
`permissions.${type}`,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
[...rootPolicyValue, {}],
|
||||
{ shouldDirty: true, shouldTouch: true }
|
||||
);
|
||||
} else if (!rootPolicyValue?.length) {
|
||||
rootForm.setValue(
|
||||
`permissions.${type}`,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
[{}],
|
||||
{
|
||||
shouldDirty: true,
|
||||
shouldTouch: true
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
})();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
placeholder="Search policies..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faSearch} />}
|
||||
rightIcon={
|
||||
search ? (
|
||||
<IconButton ariaLabel="Clear search" variant="plain" onClick={() => setSearch("")}>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<TableContainer className="thin-scrollbar mt-4 max-h-[28rem]">
|
||||
<div className="sticky top-0 z-30 flex justify-between border-b border-b-mineshaft-600 bg-mineshaft-800 py-3 pl-5 pr-4 font-inter text-sm font-medium text-bunker-300">
|
||||
<span>Resource</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="plain"
|
||||
className="p-0 text-mineshaft-400"
|
||||
size="xs"
|
||||
colorSchema="secondary"
|
||||
onClick={() => {
|
||||
setValue(
|
||||
"permissions",
|
||||
Object.fromEntries(
|
||||
filteredPolicies.map((subject) => [subject, true])
|
||||
) as TForm["permissions"],
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}}
|
||||
>
|
||||
Select All
|
||||
</Button>
|
||||
<Tooltip content="Clear selection">
|
||||
<IconButton
|
||||
ariaLabel="Clear selection"
|
||||
onClick={() => reset()}
|
||||
variant="plain"
|
||||
size="xs"
|
||||
className={`text-mineshaft-400 ${!isDirty ? "pointer-events-none opacity-50" : ""} hover:text-red`}
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmarkCircle} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<TBody>
|
||||
{filteredPolicies.map((subject) => (
|
||||
<Controller
|
||||
control={control}
|
||||
key={`permission-add-${subject}`}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Tr
|
||||
className={`${value ? "bg-mineshaft-600/30" : ""} cursor-pointer hover:bg-mineshaft-700`}
|
||||
onClick={() => onChange(!value)}
|
||||
>
|
||||
<Td className={`${value ? "text-mineshaft-100" : "text-mineshaft-300"} w-full`}>
|
||||
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
|
||||
</Td>
|
||||
<Td>
|
||||
{value ? <FontAwesomeIcon className="text-green" icon={faCheck} /> : null}
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
name={`permissions.${subject as ProjectPermissionSub}`}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!filteredPolicies.length && (
|
||||
<EmptyState
|
||||
iconSize="2x"
|
||||
icon={faSearch}
|
||||
className="!pb-4 !pt-8"
|
||||
title="No policies match search"
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
<div className="mt-8 flex space-x-4">
|
||||
<Button isDisabled={!isDirty} onClick={onSubmit}>
|
||||
Add Policies
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PolicySelectionModal = ({ isOpen, onOpenChange }: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Add Policies"
|
||||
subTitle="Select one or more policies to add to this role."
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<Content onClose={() => onOpenChange(false)} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,202 @@
|
||||
import { useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { useGetProjectTypeFromRoute } from "@app/hooks";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import {
|
||||
PROJECT_PERMISSION_OBJECT,
|
||||
RoleTemplate,
|
||||
RoleTemplates,
|
||||
TFormSchema
|
||||
} from "./ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const Content = ({ onClose }: ContentProps) => {
|
||||
const rootForm = useFormContext<TFormSchema>();
|
||||
const projectType = useGetProjectTypeFromRoute();
|
||||
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<RoleTemplate>();
|
||||
const [conflictingSubjects, setConflictingSubjects] = useState<ProjectPermissionSub[]>([]);
|
||||
const [showConflictingSubjects, setShowConflictingSubjects] = useState(false);
|
||||
|
||||
const templates = RoleTemplates[projectType ?? ProjectType.SecretManager];
|
||||
|
||||
const onSubmit = (skipConflicting = false) => {
|
||||
if (!selectedTemplate) {
|
||||
createNotification({ type: "error", text: "Please select a template" });
|
||||
return;
|
||||
}
|
||||
|
||||
selectedTemplate.permissions.forEach(({ subject, actions }) => {
|
||||
if (skipConflicting && conflictingSubjects.includes(subject)) return;
|
||||
|
||||
rootForm.setValue(
|
||||
`permissions.${subject}`,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
[Object.fromEntries(actions.map((action) => [action, true]))],
|
||||
{
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: true
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onApply = () => {
|
||||
if (!selectedTemplate) {
|
||||
createNotification({ type: "error", text: "Please select a template" });
|
||||
return;
|
||||
}
|
||||
|
||||
const conflictingPolicies: ProjectPermissionSub[] = [];
|
||||
|
||||
selectedTemplate.permissions.forEach(({ subject }) => {
|
||||
const rootPolicyValue = rootForm.getValues("permissions")?.[subject];
|
||||
|
||||
if (rootPolicyValue?.length) {
|
||||
conflictingPolicies.push(subject);
|
||||
}
|
||||
});
|
||||
|
||||
if (conflictingPolicies.length) {
|
||||
setConflictingSubjects(conflictingPolicies);
|
||||
setShowConflictingSubjects(true);
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={showConflictingSubjects} onOpenChange={setShowConflictingSubjects}>
|
||||
<ModalContent
|
||||
title="Conflicting Policies"
|
||||
subTitle="The following resources already have policies assigned to them."
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
{conflictingSubjects.map((subject) => (
|
||||
<div key={subject}>
|
||||
<span className="text-mineshaft-200">
|
||||
{PROJECT_PERMISSION_OBJECT[subject].title}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8 flex space-x-4">
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="danger" onClick={() => onSubmit()}>
|
||||
Overwrite Existing
|
||||
</Button>
|
||||
</ModalClose>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" onClick={() => onSubmit(true)}>
|
||||
Skip Conflicting
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Accordion
|
||||
type="single"
|
||||
value={selectedTemplate?.id}
|
||||
onValueChange={(value) =>
|
||||
setSelectedTemplate(templates.find((template) => template.id === value))
|
||||
}
|
||||
collapsible
|
||||
className="w-full border-collapse"
|
||||
>
|
||||
{templates.map(({ name, description, permissions, id }) => (
|
||||
<AccordionItem
|
||||
key={id}
|
||||
value={id}
|
||||
className="m-0 border border-mineshaft-600 first:rounded-t last:rounded-b data-[state=open]:border-primary/40 data-[state=open]:bg-mineshaft-600/30"
|
||||
>
|
||||
<AccordionTrigger className="w-full justify-start p-4 py-8 text-mineshaft-100 hover:bg-mineshaft-700 hover:text-mineshaft-100 data-[state=open]:bg-primary/[3%] data-[state=open]:text-mineshaft-100">
|
||||
<div className="mr-auto flex flex-col py-2 text-left">
|
||||
<span>{name}</span>
|
||||
<span className="text-sm leading-3 text-mineshaft-400">{description}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="border-t border-mineshaft-600">
|
||||
<div className="thin-scrollbar max-h-[20rem] overflow-y-auto">
|
||||
<span className="text-mineshaft-300">Grants the following permissions:</span>
|
||||
<div className="grid grid-cols-2 gap-4 py-2">
|
||||
{permissions
|
||||
.map((permission) => ({
|
||||
...permission,
|
||||
object: PROJECT_PERMISSION_OBJECT[permission.subject]
|
||||
}))
|
||||
.sort((a, b) => a.object.title.localeCompare(b.object.title))
|
||||
.map(({ subject, actions, object }) => {
|
||||
return (
|
||||
<div key={subject}>
|
||||
<span className="text-mineshaft-200">{object.title}</span>
|
||||
<ul className="mt-1 flex list-disc flex-col gap-1 pl-4">
|
||||
{actions.map((action) => (
|
||||
<li key={action}>
|
||||
{object.actions.find((a) => a.value === action)?.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
<div className="mt-8 flex space-x-4">
|
||||
<Button isDisabled={!selectedTemplate} onClick={onApply}>
|
||||
Apply Template
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PolicyTemplateModal = ({ isOpen, onOpenChange }: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Policy Templates"
|
||||
subTitle="Select a template with prepopulated policies to get started. You can always add more policies later."
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<Content onClose={() => onOpenChange(false)} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+526
-19
@@ -6,6 +6,7 @@ import { z } from "zod";
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCmekActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context";
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionKmipActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSecretSyncActions,
|
||||
@@ -25,6 +27,7 @@ import {
|
||||
TPermissionConditionOperators
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import { TProjectPermission } from "@app/hooks/api/roles/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
const GeneralPolicyActionSchema = z.object({
|
||||
read: z.boolean().optional(),
|
||||
@@ -33,6 +36,14 @@ const GeneralPolicyActionSchema = z.object({
|
||||
create: z.boolean().optional()
|
||||
});
|
||||
|
||||
const CertificatePolicyActionSchema = z.object({
|
||||
[ProjectPermissionCertificateActions.Create]: z.boolean().optional(),
|
||||
[ProjectPermissionCertificateActions.Delete]: z.boolean().optional(),
|
||||
[ProjectPermissionCertificateActions.Edit]: z.boolean().optional(),
|
||||
[ProjectPermissionCertificateActions.Read]: z.boolean().optional(),
|
||||
[ProjectPermissionCertificateActions.ReadPrivateKey]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const SecretPolicyActionSchema = z.object({
|
||||
[ProjectPermissionSecretActions.DescribeAndReadValue]: z.boolean().optional(), // existing read, gives both describe and read value
|
||||
[ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(),
|
||||
@@ -127,6 +138,15 @@ const SshHostPolicyActionSchema = z.object({
|
||||
[ProjectPermissionSshHostActions.IssueHostCert]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const PkiSubscriberPolicyActionSchema = z.object({
|
||||
[ProjectPermissionPkiSubscriberActions.Read]: z.boolean().optional(),
|
||||
[ProjectPermissionPkiSubscriberActions.Create]: z.boolean().optional(),
|
||||
[ProjectPermissionPkiSubscriberActions.Edit]: z.boolean().optional(),
|
||||
[ProjectPermissionPkiSubscriberActions.Delete]: z.boolean().optional(),
|
||||
[ProjectPermissionPkiSubscriberActions.IssueCert]: z.boolean().optional(),
|
||||
[ProjectPermissionPkiSubscriberActions.ListCerts]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const SecretRollbackPolicyActionSchema = z.object({
|
||||
read: z.boolean().optional(),
|
||||
create: z.boolean().optional()
|
||||
@@ -176,7 +196,7 @@ const ConditionSchema = z
|
||||
|
||||
export const projectRoleFormSchema = z.object({
|
||||
name: z.string().trim(),
|
||||
description: z.string().trim().optional(),
|
||||
description: z.string().trim().nullish(),
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -225,7 +245,13 @@ export const projectRoleFormSchema = z.object({
|
||||
[ProjectPermissionSub.AuditLogs]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Certificates]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.PkiSubscribers]: PkiSubscriberPolicyActionSchema.extend({
|
||||
inverted: z.boolean().optional(),
|
||||
conditions: ConditionSchema
|
||||
})
|
||||
.array()
|
||||
.default([]),
|
||||
[ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
|
||||
@@ -268,6 +294,7 @@ type TConditionalFields =
|
||||
| ProjectPermissionSub.SecretFolders
|
||||
| ProjectPermissionSub.SecretImports
|
||||
| ProjectPermissionSub.DynamicSecrets
|
||||
| ProjectPermissionSub.PkiSubscribers
|
||||
| ProjectPermissionSub.SshHosts
|
||||
| ProjectPermissionSub.SecretRotation
|
||||
| ProjectPermissionSub.Identity;
|
||||
@@ -281,7 +308,8 @@ export const isConditionalSubjects = (
|
||||
subject === ProjectPermissionSub.SecretFolders ||
|
||||
subject === ProjectPermissionSub.Identity ||
|
||||
subject === ProjectPermissionSub.SshHosts ||
|
||||
subject === ProjectPermissionSub.SecretRotation;
|
||||
subject === ProjectPermissionSub.SecretRotation ||
|
||||
subject === ProjectPermissionSub.PkiSubscribers;
|
||||
|
||||
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
|
||||
const formConditions: z.infer<typeof ConditionSchema> = [];
|
||||
@@ -378,7 +406,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
ProjectPermissionSub.AuditLogs,
|
||||
ProjectPermissionSub.IpAllowList,
|
||||
ProjectPermissionSub.CertificateAuthorities,
|
||||
ProjectPermissionSub.Certificates,
|
||||
ProjectPermissionSub.PkiAlerts,
|
||||
ProjectPermissionSub.PkiCollections,
|
||||
ProjectPermissionSub.CertificateTemplates,
|
||||
@@ -515,6 +542,25 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.Certificates) {
|
||||
const canRead = action.includes(ProjectPermissionCertificateActions.Read);
|
||||
const canEdit = action.includes(ProjectPermissionCertificateActions.Edit);
|
||||
const canDelete = action.includes(ProjectPermissionCertificateActions.Delete);
|
||||
const canCreate = action.includes(ProjectPermissionCertificateActions.Create);
|
||||
const canReadPrivateKey = action.includes(ProjectPermissionCertificateActions.ReadPrivateKey);
|
||||
|
||||
if (!formVal[subject]) formVal[subject] = [{}];
|
||||
|
||||
// from above statement we are sure it won't be undefined
|
||||
if (canRead) formVal[subject]![0].read = true;
|
||||
if (canEdit) formVal[subject]![0].edit = true;
|
||||
if (canCreate) formVal[subject]![0].create = true;
|
||||
if (canDelete) formVal[subject]![0].delete = true;
|
||||
if (canReadPrivateKey)
|
||||
formVal[subject]![0][ProjectPermissionCertificateActions.ReadPrivateKey] = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.Project) {
|
||||
const canEdit = action.includes(ProjectPermissionActions.Edit);
|
||||
const canDelete = action.includes(ProjectPermissionActions.Delete);
|
||||
@@ -695,6 +741,33 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
inverted
|
||||
});
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.PkiSubscribers) {
|
||||
if (!formVal[subject]) formVal[subject] = [];
|
||||
|
||||
formVal[subject]!.push({
|
||||
[ProjectPermissionPkiSubscriberActions.Edit]: action.includes(
|
||||
ProjectPermissionPkiSubscriberActions.Edit
|
||||
),
|
||||
[ProjectPermissionPkiSubscriberActions.Delete]: action.includes(
|
||||
ProjectPermissionPkiSubscriberActions.Delete
|
||||
),
|
||||
[ProjectPermissionPkiSubscriberActions.Create]: action.includes(
|
||||
ProjectPermissionPkiSubscriberActions.Create
|
||||
),
|
||||
[ProjectPermissionPkiSubscriberActions.Read]: action.includes(
|
||||
ProjectPermissionPkiSubscriberActions.Read
|
||||
),
|
||||
[ProjectPermissionPkiSubscriberActions.IssueCert]: action.includes(
|
||||
ProjectPermissionPkiSubscriberActions.IssueCert
|
||||
),
|
||||
[ProjectPermissionPkiSubscriberActions.ListCerts]: action.includes(
|
||||
ProjectPermissionPkiSubscriberActions.ListCerts
|
||||
),
|
||||
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
||||
inverted
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return formVal;
|
||||
@@ -857,19 +930,19 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
title: "Dynamic Secrets",
|
||||
actions: [
|
||||
{
|
||||
label: "Read root credentials",
|
||||
label: "Read Root Credentials",
|
||||
value: ProjectPermissionDynamicSecretActions.ReadRootCredential
|
||||
},
|
||||
{
|
||||
label: "Create root credentials",
|
||||
label: "Create Root Credentials",
|
||||
value: ProjectPermissionDynamicSecretActions.CreateRootCredential
|
||||
},
|
||||
{
|
||||
label: "Modify root credentials",
|
||||
label: "Modify Root Credentials",
|
||||
value: ProjectPermissionDynamicSecretActions.EditRootCredential
|
||||
},
|
||||
{
|
||||
label: "Remove root credentials",
|
||||
label: "Remove Root Credentials",
|
||||
value: ProjectPermissionDynamicSecretActions.DeleteRootCredential
|
||||
},
|
||||
{ label: "Manage Leases", value: ProjectPermissionDynamicSecretActions.Lease }
|
||||
@@ -893,7 +966,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
actions: [{ label: "Modify", value: "edit" }]
|
||||
},
|
||||
[ProjectPermissionSub.Integrations]: {
|
||||
title: "Integrations",
|
||||
title: "Native Integrations",
|
||||
actions: [
|
||||
{ label: "Read", value: "read" },
|
||||
{ label: "Create", value: "create" },
|
||||
@@ -1029,10 +1102,11 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
[ProjectPermissionSub.Certificates]: {
|
||||
title: "Certificates",
|
||||
actions: [
|
||||
{ label: "Read", value: "read" },
|
||||
{ label: "Create", value: "create" },
|
||||
{ label: "Modify", value: "edit" },
|
||||
{ label: "Remove", value: "delete" }
|
||||
{ label: "Read", value: ProjectPermissionCertificateActions.Read },
|
||||
{ label: "Read Private Key", value: ProjectPermissionCertificateActions.ReadPrivateKey },
|
||||
{ label: "Create", value: ProjectPermissionCertificateActions.Create },
|
||||
{ label: "Modify", value: ProjectPermissionCertificateActions.Edit },
|
||||
{ label: "Remove", value: ProjectPermissionCertificateActions.Delete }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.CertificateTemplates]: {
|
||||
@@ -1090,6 +1164,17 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
{ label: "Remove", value: "delete" }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.PkiSubscribers]: {
|
||||
title: "PKI Subscribers",
|
||||
actions: [
|
||||
{ label: "Read", value: ProjectPermissionPkiSubscriberActions.Read },
|
||||
{ label: "Create", value: ProjectPermissionPkiSubscriberActions.Create },
|
||||
{ label: "Modify", value: ProjectPermissionPkiSubscriberActions.Edit },
|
||||
{ label: "Remove", value: ProjectPermissionPkiSubscriberActions.Delete },
|
||||
{ label: "Issue Certificate", value: ProjectPermissionPkiSubscriberActions.IssueCert },
|
||||
{ label: "List Certificates", value: ProjectPermissionPkiSubscriberActions.ListCerts }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.PkiCollections]: {
|
||||
title: "PKI Collections",
|
||||
actions: [
|
||||
@@ -1109,7 +1194,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.SecretApproval]: {
|
||||
title: "Secret Protect policy",
|
||||
title: "Secret Approval Policies",
|
||||
actions: [
|
||||
{ label: "Read", value: "read" },
|
||||
{ label: "Create", value: "create" },
|
||||
@@ -1160,25 +1245,447 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
title: "KMIP",
|
||||
actions: [
|
||||
{
|
||||
label: "Read clients",
|
||||
label: "Read Clients",
|
||||
value: ProjectPermissionKmipActions.ReadClients
|
||||
},
|
||||
{
|
||||
label: "Create clients",
|
||||
label: "Create Clients",
|
||||
value: ProjectPermissionKmipActions.CreateClients
|
||||
},
|
||||
{
|
||||
label: "Modify clients",
|
||||
label: "Modify Clients",
|
||||
value: ProjectPermissionKmipActions.UpdateClients
|
||||
},
|
||||
{
|
||||
label: "Delete clients",
|
||||
label: "Delete Clients",
|
||||
value: ProjectPermissionKmipActions.DeleteClients
|
||||
},
|
||||
{
|
||||
label: "Generate client certificates",
|
||||
label: "Generate Client Certificates",
|
||||
value: ProjectPermissionKmipActions.GenerateClientCertificates
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const SharedPermissionSubjects = {
|
||||
[ProjectPermissionSub.AuditLogs]: true,
|
||||
[ProjectPermissionSub.Groups]: true,
|
||||
[ProjectPermissionSub.Member]: true,
|
||||
[ProjectPermissionSub.Identity]: true,
|
||||
[ProjectPermissionSub.Project]: true,
|
||||
[ProjectPermissionSub.Role]: true,
|
||||
[ProjectPermissionSub.Settings]: true
|
||||
};
|
||||
|
||||
const SecretsManagerPermissionSubjects = (enabled = false) => ({
|
||||
[ProjectPermissionSub.SecretFolders]: enabled,
|
||||
[ProjectPermissionSub.SecretImports]: enabled,
|
||||
[ProjectPermissionSub.DynamicSecrets]: enabled,
|
||||
[ProjectPermissionSub.Secrets]: enabled,
|
||||
[ProjectPermissionSub.SecretApproval]: enabled,
|
||||
[ProjectPermissionSub.Integrations]: enabled,
|
||||
[ProjectPermissionSub.SecretSyncs]: enabled,
|
||||
[ProjectPermissionSub.Kms]: enabled,
|
||||
[ProjectPermissionSub.Environments]: enabled,
|
||||
[ProjectPermissionSub.Tags]: enabled,
|
||||
[ProjectPermissionSub.Webhooks]: enabled,
|
||||
[ProjectPermissionSub.IpAllowList]: enabled,
|
||||
[ProjectPermissionSub.SecretRollback]: enabled,
|
||||
[ProjectPermissionSub.SecretRotation]: enabled,
|
||||
[ProjectPermissionSub.ServiceTokens]: enabled
|
||||
});
|
||||
|
||||
const KmsPermissionSubjects = (enabled = false) => ({
|
||||
[ProjectPermissionSub.Cmek]: enabled,
|
||||
[ProjectPermissionSub.Kmip]: enabled
|
||||
});
|
||||
|
||||
const CertificateManagerPermissionSubjects = (enabled = false) => ({
|
||||
[ProjectPermissionSub.PkiCollections]: enabled,
|
||||
[ProjectPermissionSub.PkiAlerts]: enabled,
|
||||
[ProjectPermissionSub.PkiSubscribers]: enabled,
|
||||
[ProjectPermissionSub.CertificateAuthorities]: enabled,
|
||||
[ProjectPermissionSub.CertificateTemplates]: enabled,
|
||||
[ProjectPermissionSub.Certificates]: enabled
|
||||
});
|
||||
|
||||
const SshPermissionSubjects = (enabled = false) => ({
|
||||
[ProjectPermissionSub.SshCertificateAuthorities]: enabled,
|
||||
[ProjectPermissionSub.SshCertificates]: enabled,
|
||||
[ProjectPermissionSub.SshCertificateTemplates]: enabled,
|
||||
[ProjectPermissionSub.SshHosts]: enabled,
|
||||
[ProjectPermissionSub.SshHostGroups]: enabled
|
||||
});
|
||||
|
||||
// scott: this structure ensures we don't forget to add project permissions to their relevant project type
|
||||
export const ProjectTypePermissionSubjects: Record<
|
||||
ProjectType,
|
||||
Record<ProjectPermissionSub, boolean>
|
||||
> = {
|
||||
[ProjectType.SecretManager]: {
|
||||
...SharedPermissionSubjects,
|
||||
...SecretsManagerPermissionSubjects(true),
|
||||
...KmsPermissionSubjects(),
|
||||
...CertificateManagerPermissionSubjects(),
|
||||
...SshPermissionSubjects()
|
||||
},
|
||||
[ProjectType.KMS]: {
|
||||
...SharedPermissionSubjects,
|
||||
...KmsPermissionSubjects(true),
|
||||
...SecretsManagerPermissionSubjects(),
|
||||
...CertificateManagerPermissionSubjects(),
|
||||
...SshPermissionSubjects()
|
||||
},
|
||||
[ProjectType.CertificateManager]: {
|
||||
...SharedPermissionSubjects,
|
||||
...CertificateManagerPermissionSubjects(true),
|
||||
...KmsPermissionSubjects(),
|
||||
...SecretsManagerPermissionSubjects(),
|
||||
...SshPermissionSubjects()
|
||||
},
|
||||
[ProjectType.SSH]: {
|
||||
...SharedPermissionSubjects,
|
||||
...SshPermissionSubjects(true),
|
||||
...CertificateManagerPermissionSubjects(),
|
||||
...KmsPermissionSubjects(),
|
||||
...SecretsManagerPermissionSubjects()
|
||||
}
|
||||
};
|
||||
|
||||
export type RoleTemplate = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
permissions: { subject: ProjectPermissionSub; actions: string[] }[];
|
||||
};
|
||||
|
||||
const projectManagerTemplate = (
|
||||
additionalPermissions: RoleTemplate["permissions"] = []
|
||||
): RoleTemplate => ({
|
||||
id: "project-manager",
|
||||
name: "Project Management Policies",
|
||||
description: "Grants access to manage project members and settings",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.AuditLogs,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Groups,
|
||||
actions: Object.values(ProjectPermissionGroupActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Member,
|
||||
actions: Object.values(ProjectPermissionMemberActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Identity,
|
||||
actions: Object.values(ProjectPermissionIdentityActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Project,
|
||||
actions: [ProjectPermissionActions.Edit, ProjectPermissionActions.Delete]
|
||||
},
|
||||
{ subject: ProjectPermissionSub.Role, actions: Object.values(ProjectPermissionActions) },
|
||||
{
|
||||
subject: ProjectPermissionSub.Settings,
|
||||
actions: [ProjectPermissionActions.Read, ProjectPermissionActions.Edit]
|
||||
},
|
||||
...additionalPermissions
|
||||
]
|
||||
});
|
||||
|
||||
export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
|
||||
[ProjectType.SSH]: [
|
||||
{
|
||||
id: "ssh-viewer",
|
||||
name: "SSH Viewing Policies",
|
||||
description: "Grants read access to SSH certificates and hosts",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.SshCertificateAuthorities,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SshCertificates,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SshCertificateTemplates,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SshHosts,
|
||||
actions: [ProjectPermissionSshHostActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SshHostGroups,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "ssh-cert-editor",
|
||||
name: "SSH Certificate Editing Policies",
|
||||
description: "Grants read and edit access to SSH certificates",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.SshCertificateAuthorities,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SshCertificates,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SshCertificateTemplates,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "ssh-host-editor",
|
||||
name: "SSH Host Editing Policies",
|
||||
description: "Grants read and edit access to SSH hosts",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.SshHosts,
|
||||
actions: Object.values(ProjectPermissionSshHostActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SshHostGroups,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
}
|
||||
]
|
||||
},
|
||||
projectManagerTemplate()
|
||||
],
|
||||
[ProjectType.KMS]: [
|
||||
{
|
||||
id: "kms-viewer",
|
||||
name: "KMS Viewing Policies",
|
||||
description: "Grants read access to KMS keys and KMIP clients",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.Cmek,
|
||||
actions: [ProjectPermissionCmekActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Kmip,
|
||||
actions: [ProjectPermissionKmipActions.ReadClients]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "key-editor",
|
||||
name: "KMS Key Editing Policies",
|
||||
description: "Grants read and edit access to KMS keys",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.Cmek,
|
||||
actions: Object.values(ProjectPermissionCmekActions)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "kmip-editor",
|
||||
name: "KMIP Client Editing Policies",
|
||||
description: "Grants read and edit access to KMIP clients",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.Kmip,
|
||||
actions: Object.values(ProjectPermissionKmipActions)
|
||||
}
|
||||
]
|
||||
},
|
||||
projectManagerTemplate()
|
||||
],
|
||||
[ProjectType.CertificateManager]: [
|
||||
{
|
||||
id: "cert-viewer",
|
||||
name: "Certificate Viewing Policies",
|
||||
description: "Grants read access to certificates and related resources",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.PkiCollections,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.PkiAlerts,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.CertificateAuthorities,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.CertificateTemplates,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Certificates,
|
||||
actions: [
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "cert-editor",
|
||||
name: "Certificate Editing Policies",
|
||||
description: "Grants read and edit access to certificates and related resources",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.PkiCollections,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.PkiAlerts,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.CertificateAuthorities,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.CertificateTemplates,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Certificates,
|
||||
actions: Object.values(ProjectPermissionCertificateActions)
|
||||
}
|
||||
]
|
||||
},
|
||||
projectManagerTemplate()
|
||||
],
|
||||
[ProjectType.SecretManager]: [
|
||||
{
|
||||
id: "secret-viewer",
|
||||
name: "Secret Viewing Policies",
|
||||
description: "Grants read access to secrets and related resources",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretRollback,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretImports,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Secrets,
|
||||
actions: [
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
ProjectPermissionSecretActions.ReadValue
|
||||
]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.DynamicSecrets,
|
||||
actions: [ProjectPermissionDynamicSecretActions.ReadRootCredential]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Environments,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Tags,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretRotation,
|
||||
actions: [ProjectPermissionSecretRotationActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Integrations,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretSyncs,
|
||||
actions: [ProjectPermissionSecretSyncActions.Read]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "secret-editor",
|
||||
name: "Secret Editing Policies",
|
||||
description: "Grants read and edit access to secrets and related resources",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.Environments,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.DynamicSecrets,
|
||||
actions: Object.values(ProjectPermissionDynamicSecretActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Secrets,
|
||||
actions: [
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
ProjectPermissionSecretActions.Create,
|
||||
ProjectPermissionSecretActions.Delete
|
||||
]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretRollback,
|
||||
actions: [ProjectPermissionActions.Read, ProjectPermissionActions.Create]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Tags,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretImports,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretRotation,
|
||||
actions: Object.values(ProjectPermissionSecretRotationActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretFolders,
|
||||
actions: [
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Delete
|
||||
]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Integrations,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretSyncs,
|
||||
actions: Object.values(ProjectPermissionSecretSyncActions)
|
||||
}
|
||||
]
|
||||
},
|
||||
projectManagerTemplate([
|
||||
{
|
||||
subject: ProjectPermissionSub.IpAllowList,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Kms,
|
||||
actions: [ProjectPermissionActions.Edit]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretApproval,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.ServiceTokens,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Webhooks,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
}
|
||||
])
|
||||
]
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { IconButton, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { useGetProjectRoleBySlug } from "@app/hooks/api";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
@@ -21,7 +22,9 @@ export const RoleDetailsSection = ({ roleSlug, handlePopUpOpen }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data } = useGetProjectRoleBySlug(currentWorkspace?.id ?? "", roleSlug as string);
|
||||
|
||||
const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(data?.slug ?? "");
|
||||
const isCustomRole = !Object.values(ProjectMembershipRole).includes(
|
||||
(data?.slug ?? "") as ProjectMembershipRole
|
||||
);
|
||||
|
||||
return data ? (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
|
||||
@@ -17,7 +17,7 @@ import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
name: z.string().min(1, "Name required"),
|
||||
description: z.string(),
|
||||
slug: slugSchema({ min: 1 })
|
||||
})
|
||||
@@ -62,7 +62,7 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
if (role) {
|
||||
reset({
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
description: role.description || "",
|
||||
slug: role.slug
|
||||
});
|
||||
} else {
|
||||
@@ -115,12 +115,8 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
});
|
||||
|
||||
reset();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = err as any;
|
||||
const text =
|
||||
error?.response?.data?.message ??
|
||||
`Failed to ${popUp?.role?.data ? "update" : "create"} role`;
|
||||
} catch {
|
||||
const text = `Failed to ${popUp?.role?.data ? "update" : "create"} role`;
|
||||
|
||||
createNotification({
|
||||
text,
|
||||
|
||||
+31
-80
@@ -1,36 +1,34 @@
|
||||
import { useMemo } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { MongoAbility, MongoQuery, RawRuleOf } from "@casl/ability";
|
||||
import { faPlus, faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { AccessTree } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/v2";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||
import { evaluatePermissionsAbility } from "@app/helpers/permissions";
|
||||
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { AddPoliciesButton } from "./AddPoliciesButton";
|
||||
import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions";
|
||||
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
|
||||
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
|
||||
import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions";
|
||||
import { PermissionEmptyState } from "./PermissionEmptyState";
|
||||
import { PkiSubscriberPermissionConditions } from "./PkiSubscriberPermissionConditions";
|
||||
import {
|
||||
formRolePermission2API,
|
||||
isConditionalSubjects,
|
||||
PROJECT_PERMISSION_OBJECT,
|
||||
projectRoleFormSchema,
|
||||
ProjectTypePermissionSubjects,
|
||||
rolePermission2Form,
|
||||
TFormSchema
|
||||
} from "./ProjectRoleModifySection.utils";
|
||||
@@ -61,6 +59,10 @@ export const renderConditionalComponents = (
|
||||
return <SshHostPermissionConditions isDisabled={isDisabled} />;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.PkiSubscribers) {
|
||||
return <PkiSubscriberPermissionConditions isDisabled={isDisabled} />;
|
||||
}
|
||||
|
||||
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
|
||||
}
|
||||
|
||||
@@ -104,31 +106,9 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(role?.slug ?? "");
|
||||
|
||||
const onNewPolicy = (selectedSubject: ProjectPermissionSub) => {
|
||||
const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`);
|
||||
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
|
||||
form.setValue(
|
||||
`permissions.${selectedSubject}`,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
[...rootPolicyValue, {}],
|
||||
{ shouldDirty: true, shouldTouch: true }
|
||||
);
|
||||
} else {
|
||||
form.setValue(
|
||||
`permissions.${selectedSubject}`,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
[{}],
|
||||
{
|
||||
shouldDirty: true,
|
||||
shouldTouch: true
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
const isCustomRole = !Object.values(ProjectMembershipRole).includes(
|
||||
(role?.slug ?? "") as ProjectMembershipRole
|
||||
);
|
||||
|
||||
const permissions = form.watch("permissions");
|
||||
|
||||
@@ -172,48 +152,17 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")}
|
||||
className={twMerge(
|
||||
"mr-4 h-10 border border-primary",
|
||||
isDirty && "bg-primary text-black"
|
||||
)}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
className="h-10 rounded-l-none"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
New policy
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="thin-scrollbar max-h-96" align="end">
|
||||
{Object.keys(PROJECT_PERMISSION_OBJECT)
|
||||
.sort((a, b) =>
|
||||
PROJECT_PERMISSION_OBJECT[
|
||||
a as keyof typeof PROJECT_PERMISSION_OBJECT
|
||||
].title
|
||||
.toLowerCase()
|
||||
.localeCompare(
|
||||
PROJECT_PERMISSION_OBJECT[
|
||||
b as keyof typeof PROJECT_PERMISSION_OBJECT
|
||||
].title.toLowerCase()
|
||||
)
|
||||
)
|
||||
.map((subject) => (
|
||||
<DropdownMenuItem
|
||||
key={`permission-create-${subject}`}
|
||||
className="py-3"
|
||||
onClick={() => onNewPolicy(subject as ProjectPermissionSub)}
|
||||
>
|
||||
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<AddPoliciesButton isDisabled={isDisabled} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -221,17 +170,19 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||
</div>
|
||||
<div className="py-4">
|
||||
{!isPending && <PermissionEmptyState />}
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
||||
<GeneralPermissionPolicies
|
||||
subject={subject}
|
||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||
key={`project-permission-${subject}`}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{renderConditionalComponents(subject, isDisabled)}
|
||||
</GeneralPermissionPolicies>
|
||||
))}
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[])
|
||||
.filter((subject) => ProjectTypePermissionSubjects[currentWorkspace.type][subject])
|
||||
.map((subject) => (
|
||||
<GeneralPermissionPolicies
|
||||
subject={subject}
|
||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||
key={`project-permission-${subject}`}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{renderConditionalComponents(subject, isDisabled)}
|
||||
</GeneralPermissionPolicies>
|
||||
))}
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
|
||||
+1
-1
@@ -125,7 +125,7 @@ export const SshHostPermissionConditions = ({ position = 0, isDisabled }: Props)
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Input {...field} />
|
||||
<Input {...field} onChange={(e) => field.onChange(e.target.value.trim())} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user