Fix merge conflicts

This commit is contained in:
Tuan Dang
2025-05-08 09:07:28 -07:00
169 changed files with 3698 additions and 1983 deletions
@@ -24,10 +24,10 @@ import {
Tooltip
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { formatProjectRoleName } from "@app/helpers/roles";
import { usePopUp } from "@app/hooks";
import { useGetProjectRoles, useUpdateGroupWorkspaceRole } from "@app/hooks/api";
import { TGroupMembership } from "@app/hooks/api/groups/types";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types";
import { groupBy } from "@app/lib/fn/array";
@@ -260,12 +260,6 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember
}
};
const formatRoleName = (role: string, customRoleName?: string) => {
if (role === ProjectMembershipRole.Custom) return customRoleName;
if (role === ProjectMembershipRole.Member) return "Developer";
return role;
};
return (
<div className="flex items-center space-x-2">
{roles
@@ -275,7 +269,7 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember
return (
<Tag key={id} className="capitalize">
<div className="flex items-center space-x-2">
<div>{formatRoleName(role, customRoleName)}</div>
<div>{formatProjectRoleName(role, customRoleName)}</div>
{isTemporary && (
<div>
<Tooltip content={isExpired ? "Expired Temporary Access" : "Temporary Access"}>
@@ -303,7 +297,7 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember
return (
<Tag key={id} className="capitalize">
<div className="flex items-center space-x-2">
<div>{formatRoleName(role, customRoleName)}</div>
<div>{formatProjectRoleName(role, customRoleName)}</div>
{isTemporary && (
<div>
<Tooltip
@@ -41,11 +41,11 @@ import {
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { formatProjectRoleName } from "@app/helpers/roles";
import { withProjectPermission } from "@app/hoc";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectIdentityOrderBy } from "@app/hooks/api/workspace/types";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -53,12 +53,6 @@ import { IdentityModal } from "./components/IdentityModal";
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
const formatRoleName = (role: string, customRoleName?: string) => {
if (role === ProjectMembershipRole.Custom) return customRoleName;
if (role === ProjectMembershipRole.Member) return "Developer";
if (role === ProjectMembershipRole.NoAccess) return "No access";
return role;
};
export const IdentityTab = withProjectPermission(
() => {
const { currentWorkspace } = useWorkspace();
@@ -286,7 +280,7 @@ export const IdentityTab = withProjectPermission(
<Tag key={roleId}>
<div className="flex items-center space-x-2">
<div className="capitalize">
{formatRoleName(role, customRoleName)}
{formatProjectRoleName(role, customRoleName)}
</div>
{isTemporary && (
<div>
@@ -331,7 +325,9 @@ export const IdentityTab = withProjectPermission(
return (
<Tag key={roleId} className="capitalize">
<div className="flex items-center space-x-2">
<div>{formatRoleName(role, customRoleName)}</div>
<div>
{formatProjectRoleName(role, customRoleName)}
</div>
{isTemporary && (
<div>
<Tooltip
@@ -50,19 +50,13 @@ import {
useUser,
useWorkspace
} from "@app/context";
import { formatProjectRoleName } from "@app/helpers/roles";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { useGetProjectRoles, useGetWorkspaceUsers } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
const formatRoleName = (role: string, customRoleName?: string) => {
if (role === ProjectMembershipRole.Custom) return customRoleName;
if (role === ProjectMembershipRole.Member) return "Developer";
if (role === ProjectMembershipRole.NoAccess) return "No access";
return role;
};
type Props = {
handlePopUpOpen: (
@@ -343,7 +337,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
<Tag key={id}>
<div className="flex items-center space-x-2">
<div className="capitalize">
{formatRoleName(role, customRoleName)}
{formatProjectRoleName(role, customRoleName)}
</div>
{isTemporary && (
<div>
@@ -386,7 +380,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
return (
<Tag key={id} className="capitalize">
<div className="flex items-center space-x-2">
<div>{formatRoleName(role, customRoleName)}</div>
<div>{formatProjectRoleName(role, customRoleName)}</div>
{isTemporary && (
<div>
<Tooltip
@@ -24,7 +24,7 @@ import {
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api";
import { TProjectRole } from "@app/hooks/api/roles/types";
import { ProjectMembershipRole, TProjectRole } from "@app/hooks/api/roles/types";
import { RoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/RoleModal";
export const ProjectRoleList = () => {
@@ -86,7 +86,9 @@ export const ProjectRoleList = () => {
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
{roles?.map((role) => {
const { id, name, slug } = role;
const isNonMutatable = ["admin", "member", "viewer", "no-access"].includes(slug);
const isNonMutatable = Object.values(ProjectMembershipRole).includes(
slug as ProjectMembershipRole
);
return (
<Tr
@@ -10,10 +10,6 @@ import { AxiosError } from "axios";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
decryptAssymmetric,
encryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import {
Button,
Checkbox,
@@ -28,7 +24,7 @@ import {
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
import { useCreateServiceToken, useGetUserWsKey } from "@app/hooks/api";
import { useCreateServiceToken } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const apiTokenExpiry = [
@@ -97,7 +93,6 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
const [newToken, setToken] = useState("");
const [isTokenCopied, setIsTokenCopied] = useToggle(false);
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?.id ?? "");
const createServiceToken = useCreateServiceToken();
const hasServiceToken = Boolean(newToken);
@@ -118,26 +113,13 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
const onFormSubmit = async ({ name, scopes, expiresIn, permissions }: FormData) => {
try {
if (!currentWorkspace?.id) return;
if (!latestFileKey) return;
const key = decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: localStorage.getItem("PRIVATE_KEY") as string
});
const randomBytes = crypto.randomBytes(16).toString("hex");
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: key,
key: randomBytes
});
const { serviceToken } = await createServiceToken.mutateAsync({
encryptedKey: ciphertext,
iv,
tag,
encryptedKey: "",
iv: "",
tag: "",
scopes,
expiresIn: Number(expiresIn),
name,
@@ -18,10 +18,6 @@ import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FormControl,
FormLabel,
Input,
@@ -37,6 +33,7 @@ import {
useProjectPermission,
useWorkspace
} from "@app/context";
import { usePopUp } from "@app/hooks";
import {
useCreateIdentityProjectAdditionalPrivilege,
useGetIdentityProjectPrivilegeDetails,
@@ -45,9 +42,9 @@ import {
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/identityProjectAdditionalPrivilege/types";
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
import {
formRolePermission2API,
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT,
projectRoleFormSchema,
rolePermission2Form
@@ -100,6 +97,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
const form = useForm<TFormSchema>({
values: privilegeDetails
@@ -194,30 +192,6 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
}
}
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
}
);
}
};
return (
<form
onSubmit={handleSubmit(onSubmit)}
@@ -249,46 +223,25 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
<Button
variant="outline_bg"
type="submit"
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")}
className={twMerge(
"h-10 rounded-r-none border border-primary",
isDirty && "bg-primary text-black"
)}
isDisabled={isSubmitting || !isDirty || isDisabled}
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((permissionSubject) => (
<DropdownMenuItem
key={`permission-create-${permissionSubject}`}
className="py-3"
onClick={() => onNewPolicy(permissionSubject as ProjectPermissionSub)}
>
{PROJECT_PERMISSION_OBJECT[permissionSubject as ProjectPermissionSub].title}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<Button
isDisabled={isDisabled}
className="h-10 rounded-l-none"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpToggle("addPolicy")}
>
Add Policies
</Button>
</div>
</div>
</div>
@@ -429,6 +382,10 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
)
)}
</div>
<PolicySelectionModal
isOpen={popUp.addPolicy.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
/>
</FormProvider>
</form>
);
@@ -17,10 +17,6 @@ import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FormControl,
FormLabel,
Input,
@@ -36,6 +32,7 @@ import {
useProjectPermission,
useWorkspace
} from "@app/context";
import { usePopUp } from "@app/hooks";
import {
useCreateProjectUserAdditionalPrivilege,
useGetProjectUserPrivilegeDetails,
@@ -44,9 +41,9 @@ import {
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/projectUserAdditionalPrivilege/types";
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
import {
formRolePermission2API,
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT,
projectRoleFormSchema,
rolePermission2Form
@@ -86,6 +83,8 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
projectMembershipId,
isDisabled
}: Props) => {
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
const isCreate = !privilegeId;
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
@@ -166,30 +165,6 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
}
};
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 privilegeTemporaryAccess = form.watch("temporaryAccess");
const isTemporary = privilegeTemporaryAccess?.isTemporary;
const isExpired =
@@ -245,46 +220,25 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
<Button
variant="outline_bg"
type="submit"
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")}
className={twMerge(
"h-10 rounded-r-none border border-primary",
isDirty && "bg-primary text-black"
)}
isDisabled={isSubmitting || !isDirty || isDisabled}
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>
<Button
isDisabled={isDisabled}
className="h-10 rounded-l-none"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpToggle("addPolicy")}
>
Add Policies
</Button>
</div>
</div>
</div>
@@ -423,6 +377,10 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
</GeneralPermissionPolicies>
))}
</div>
<PolicySelectionModal
isOpen={popUp.addPolicy.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
/>
</FormProvider>
</form>
);
@@ -17,6 +17,7 @@ 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 { ProjectAccessControlTabs } from "@app/types/project";
@@ -77,7 +78,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">
@@ -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>
);
};
@@ -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>
);
};
@@ -26,6 +26,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(),
@@ -957,7 +958,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" },
@@ -1178,7 +1179,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" },
@@ -1251,3 +1252,87 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
]
}
};
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.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()
}
};
@@ -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">
@@ -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,
@@ -8,18 +8,15 @@ 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 { usePopUp } from "@app/hooks";
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions";
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
@@ -32,6 +29,7 @@ import {
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT,
projectRoleFormSchema,
ProjectTypePermissionSubjects,
rolePermission2Form,
TFormSchema
} from "./ProjectRoleModifySection.utils";
@@ -93,6 +91,8 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
const { mutateAsync: updateRole } = useUpdateProjectRole();
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
const onSubmit = async (el: TFormSchema) => {
try {
if (!projectId || !role?.id) return;
@@ -109,31 +109,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");
@@ -177,48 +155,25 @@ 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(
"h-10 rounded-r-none 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>
<Button
isDisabled={isDisabled}
className="h-10 rounded-l-none"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpToggle("addPolicy")}
>
Add Policy
</Button>
</div>
</>
)}
@@ -226,18 +181,24 @@ 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>
<PolicySelectionModal
isOpen={popUp.addPolicy.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
/>
</FormProvider>
</form>
</div>