diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts index 4c842d864..383532c17 100644 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts @@ -158,7 +158,11 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime; const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange; const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - ...data, + slug: data.slug, + permissions: data.permissions, + isTemporary: data.isTemporary, + temporaryRange: data.temporaryRange, + temporaryMode: data.temporaryMode, temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) }); @@ -169,7 +173,8 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ } const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - ...data, + slug: data.slug, + permissions: data.permissions, isTemporary: false, temporaryAccessStartTime: null, temporaryAccessEndTime: null, diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index cdad71e9f..140d49536 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -1,5 +1,5 @@ import { TOrgRole } from "../roles/types"; -import { Workspace } from "../workspace/types"; +import { ProjectUserMembershipTemporaryMode, Workspace } from "../workspace/types"; import { IdentityAuthMethod } from "./enums"; export type IdentityTrustedIp = { @@ -66,7 +66,7 @@ export type IdentityMembership = { | { isTemporary: true; temporaryRange: string; - temporaryMode: string; + temporaryMode: ProjectUserMembershipTemporaryMode; temporaryAccessEndTime: string; temporaryAccessStartTime: string; } diff --git a/frontend/src/hooks/api/identityProjectAdditionalPrivilege/mutation.tsx b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/mutation.tsx index bb3f6ca88..ab8a2e5d1 100644 --- a/frontend/src/hooks/api/identityProjectAdditionalPrivilege/mutation.tsx +++ b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/mutation.tsx @@ -15,16 +15,11 @@ export const useCreateIdentityProjectAdditionalPrivilege = () => { return useMutation({ mutationFn: async (dto) => { - const { data } = await apiRequest.post( - "/api/v1/additional-privilege/identity/permanent", - dto - ); + const { data } = await apiRequest.post("/api/v2/identity-project-additional-privilege", dto); return data.privilege; }, - onSuccess: (_, { projectSlug, identityId }) => { - queryClient.invalidateQueries( - identitiyProjectPrivilegeKeys.list({ projectSlug, identityId }) - ); + onSuccess: (_, { projectId, identityId }) => { + queryClient.invalidateQueries(identitiyProjectPrivilegeKeys.list({ projectId, identityId })); } }); }; @@ -33,19 +28,22 @@ export const useUpdateIdentityProjectAdditionalPrivilege = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ privilegeSlug, projectSlug, identityId, privilegeDetails }) => { - const { data: res } = await apiRequest.patch("/api/v1/additional-privilege/identity", { - privilegeSlug, - projectSlug, - identityId, - privilegeDetails - }); + mutationFn: async ({ projectId, privilegeId, identityId, permissions, slug, type }) => { + const { data: res } = await apiRequest.patch( + `/api/v2/identity-project-additional-privilege/${privilegeId}`, + { + privilegeId, + projectId, + identityId, + permissions, + slug, + type + } + ); return res.privilege; }, - onSuccess: (_, { projectSlug, identityId }) => { - queryClient.invalidateQueries( - identitiyProjectPrivilegeKeys.list({ projectSlug, identityId }) - ); + onSuccess: (_, { projectId, identityId }) => { + queryClient.invalidateQueries(identitiyProjectPrivilegeKeys.list({ projectId, identityId })); } }); }; @@ -54,20 +52,21 @@ export const useDeleteIdentityProjectAdditionalPrivilege = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ identityId, projectSlug, privilegeSlug }) => { - const { data } = await apiRequest.delete("/api/v1/additional-privilege/identity", { - data: { - identityId, - projectSlug, - privilegeSlug + mutationFn: async ({ identityId, projectId, privilegeId }) => { + const { data } = await apiRequest.delete( + `/api/v2/identity-project-additional-privilege/${privilegeId}`, + { + data: { + identityId, + privilegeId, + projectId + } } - }); + ); return data.privilege; }, - onSuccess: (_, { projectSlug, identityId }) => { - queryClient.invalidateQueries( - identitiyProjectPrivilegeKeys.list({ projectSlug, identityId }) - ); + onSuccess: (_, { projectId, identityId }) => { + queryClient.invalidateQueries(identitiyProjectPrivilegeKeys.list({ projectId, identityId })); } }); }; diff --git a/frontend/src/hooks/api/identityProjectAdditionalPrivilege/queries.tsx b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/queries.tsx index e4bd141fb..8504f0423 100644 --- a/frontend/src/hooks/api/identityProjectAdditionalPrivilege/queries.tsx +++ b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/queries.tsx @@ -9,36 +9,36 @@ import { } from "./types"; export const identitiyProjectPrivilegeKeys = { - details: ({ identityId, privilegeSlug, projectSlug }: TGetIdentityProjectPrivilegeDetails) => + details: ({ identityId, privilegeId, projectId }: TGetIdentityProjectPrivilegeDetails) => [ "identity-user-privilege", { identityId, - projectSlug, - privilegeSlug + projectId, + privilegeId } ] as const, - list: ({ projectSlug, identityId }: TListIdentityProjectPrivileges) => - ["identity-user-privileges", { identityId, projectSlug }] as const + list: ({ projectId, identityId }: TListIdentityProjectPrivileges) => + ["identity-user-privileges", { identityId, projectId }] as const }; export const useGetIdentityProjectPrivilegeDetails = ({ - projectSlug, + projectId, identityId, - privilegeSlug + privilegeId }: TGetIdentityProjectPrivilegeDetails) => { return useQuery({ - enabled: Boolean(projectSlug && identityId && privilegeSlug), - queryKey: identitiyProjectPrivilegeKeys.details({ projectSlug, privilegeSlug, identityId }), + enabled: Boolean(projectId && identityId && privilegeId), + queryKey: identitiyProjectPrivilegeKeys.details({ projectId, privilegeId, identityId }), queryFn: async () => { const { data: { privilege } } = await apiRequest.get<{ privilege: TIdentityProjectPrivilege; - }>(`/api/v1/additional-privilege/identity/${privilegeSlug}`, { + }>(`/api/v2/identity-project-additional-privilege/${privilegeId}`, { params: { identityId, - projectSlug + projectId } }); return privilege; @@ -47,19 +47,19 @@ export const useGetIdentityProjectPrivilegeDetails = ({ }; export const useListIdentityProjectPrivileges = ({ - projectSlug, + projectId, identityId }: TListIdentityProjectPrivileges) => { return useQuery({ - enabled: Boolean(projectSlug && identityId), - queryKey: identitiyProjectPrivilegeKeys.list({ projectSlug, identityId }), + enabled: Boolean(projectId && identityId), + queryKey: identitiyProjectPrivilegeKeys.list({ projectId, identityId }), queryFn: async () => { const { data: { privileges } } = await apiRequest.get<{ privileges: Array; - }>("/api/v1/additional-privilege/identity", { - params: { identityId, projectSlug } + }>("/api/v2/identity-project-additional-privilege", { + params: { identityId, projectId } }); return privileges; } diff --git a/frontend/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx index df04f3e8a..1e070e2bb 100644 --- a/frontend/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx +++ b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx @@ -28,48 +28,42 @@ export type TIdentityProjectPrivilege = { } ); -export type TProjectSpecificPrivilegePermission = { - conditions: { - environment: string; - secretPath?: { $glob: string }; - }; - actions: string[]; - subject: string; -}; - export type TCreateIdentityProjectPrivilegeDTO = { identityId: string; - projectSlug: string; + projectId: string; slug?: string; - isTemporary?: boolean; - temporaryMode?: IdentityProjectAdditionalPrivilegeTemporaryMode; - temporaryRange?: string; - temporaryAccessStartTime?: string; - privilegePermission: TProjectSpecificPrivilegePermission; + type: + | { + isTemporary: true; + temporaryMode?: IdentityProjectAdditionalPrivilegeTemporaryMode; + temporaryRange?: string; + temporaryAccessStartTime?: string; + } + | { + isTemporary: false; + }; + permissions: TProjectPermission[]; }; export type TUpdateIdentityProjectPrivlegeDTO = { - projectSlug: string; + projectId: string; identityId: string; - privilegeSlug: string; - privilegeDetails: Partial< - Omit - >; -}; + privilegeId: string; +} & Partial>; export type TDeleteIdentityProjectPrivilegeDTO = { - projectSlug: string; + projectId: string; identityId: string; - privilegeSlug: string; + privilegeId: string; }; export type TListIdentityUserPrivileges = { - projectSlug: string; + projectId: string; identityId: string; }; export type TGetIdentityProejctPrivilegeDetails = { - projectSlug: string; + projectId: string; identityId: string; - privilegeSlug: string; + privilegeId: string; }; diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx index bff2d7150..8d44d8b74 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/workspace/index.tsx @@ -18,6 +18,7 @@ export { useGetUserWorkspaces, useGetWorkspaceAuthorizations, useGetWorkspaceById, + useGetWorkspaceIdentityMembershipDetails, useGetWorkspaceIdentityMemberships, useGetWorkspaceIndexStatus, useGetWorkspaceIntegrations, diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 391b2ab6a..7b190092c 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -9,7 +9,7 @@ import { TCertificate } from "../certificates/types"; import { TCertificateTemplate } from "../certificateTemplates/types"; import { TGroupMembership } from "../groups/types"; import { identitiesKeys } from "../identities/queries"; -import { TProjectIdentitiesList } from "../identities/types"; +import { IdentityMembership, TProjectIdentitiesList } from "../identities/types"; import { IntegrationAuth } from "../integrationAuth/types"; import { TIntegration } from "../integrations/types"; import { TPkiAlert } from "../pkiAlerts/types"; @@ -477,6 +477,9 @@ export const useUpdateIdentityWorkspaceRole = () => { onSuccess: (_, { identityId, workspaceId }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceIdentityMemberships(workspaceId)); queryClient.invalidateQueries(identitiesKeys.getIdentityProjectMemberships(identityId)); + queryClient.invalidateQueries( + workspaceKeys.getWorkspaceIdentityMembershipDetails(workspaceId, identityId) + ); } }); }; @@ -553,6 +556,21 @@ export const useGetWorkspaceIdentityMemberships = ( }); }; +export const useGetWorkspaceIdentityMembershipDetails = (projectId: string, identityId: string) => { + return useQuery({ + enabled: Boolean(projectId && identityId), + queryKey: workspaceKeys.getWorkspaceIdentityMembershipDetails(projectId, identityId), + queryFn: async () => { + const { + data: { identityMembership } + } = await apiRequest.get<{ identityMembership: IdentityMembership }>( + `/api/v2/workspace/${projectId}/identity-memberships/${identityId}` + ); + return identityMembership; + } + }); +}; + export const useListWorkspaceGroups = (projectId: string) => { return useQuery({ queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectId), diff --git a/frontend/src/hooks/api/workspace/query-keys.tsx b/frontend/src/hooks/api/workspace/query-keys.tsx index 8de881cff..f5a02ec2b 100644 --- a/frontend/src/hooks/api/workspace/query-keys.tsx +++ b/frontend/src/hooks/api/workspace/query-keys.tsx @@ -19,6 +19,8 @@ export const workspaceKeys = { [{ workspaceId, membershipId }, "workspace-user-details"] as const, getWorkspaceIdentityMemberships: (workspaceId: string) => [{ workspaceId }, "workspace-identity-memberships"] as const, + getWorkspaceIdentityMembershipDetails: (workspaceId: string, identityId: string) => + [{ workspaceId, identityId }, "workspace-identity-membership-details"] as const, // allows invalidation using above key without knowing params getWorkspaceIdentityMembershipsWithParams: ({ workspaceId, diff --git a/frontend/src/pages/project/[id]/identities/[identityId]/index.tsx b/frontend/src/pages/project/[id]/identities/[identityId]/index.tsx new file mode 100644 index 000000000..9161d4d47 --- /dev/null +++ b/frontend/src/pages/project/[id]/identities/[identityId]/index.tsx @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { IdentityDetailsPage } from "@app/views/Project/IdentityDetailsPage"; + +export default function ProjectIdentityDetailsPage() { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("settings.members.title") })} + + + + + ); +} + +ProjectIdentityDetailsPage.requireAuth = true; diff --git a/frontend/src/views/Project/IdentityDetailsPage/IdentityDetailPage.tsx b/frontend/src/views/Project/IdentityDetailsPage/IdentityDetailPage.tsx new file mode 100644 index 000000000..6743a89de --- /dev/null +++ b/frontend/src/views/Project/IdentityDetailsPage/IdentityDetailPage.tsx @@ -0,0 +1,112 @@ +import { useRouter } from "next/router"; +import { faChevronLeft } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Button, EmptyState, Spinner } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { withProjectPermission } from "@app/hoc"; +import { useGetWorkspaceIdentityMembershipDetails } from "@app/hooks/api"; + +import { IdentityProjectAdditionalPrivilegeSection } from "./components/IdentityProjectAdditionalPrivilegeSection"; +import { IdentityRoleDetailsSection } from "./components/IdentityRoleDetailsSectiona"; + +export const IdentityDetailsPage = withProjectPermission( + () => { + const router = useRouter(); + const { currentWorkspace } = useWorkspace(); + + const workspaceId = currentWorkspace?.id || ""; + const identityId = router.query.identityId as string; + + const { data: identityMembershipDetails, isLoading: isMembershipDetailsLoading } = + useGetWorkspaceIdentityMembershipDetails(workspaceId, identityId); + + if (isMembershipDetailsLoading) { + return ( +
+ +
+ ); + } + + return ( +
+
+ +
+ {identityMembershipDetails ? ( + <> +
+
+
+

+ Project Identity Access +

+
+ + {(isAllowed) => ( + + )} + +
+
+
+
+ Name + {identityMembershipDetails && ( +

+ {identityMembershipDetails?.identity?.name} +

+ )} +
+
+
+ Joined on{" "} + {identityMembershipDetails?.createdAt && + format(new Date(identityMembershipDetails?.createdAt || ""), "yyyy-MM-dd")} +
+
+
+ + + + ) : ( + + )} +
+ ); + }, + { + action: ProjectPermissionActions.Read, + subject: ProjectPermissionSub.Identity + } +); diff --git a/frontend/src/views/Project/IdentityDetailsPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx new file mode 100644 index 000000000..67a117451 --- /dev/null +++ b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx @@ -0,0 +1,391 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { + faCaretDown, + faChevronLeft, + faClock, + faPlus, + faSave +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { format, formatDistance } from "date-fns"; +import ms from "ms"; +import { twMerge } from "tailwind-merge"; +import { z } from "zod"; + +import { TtlFormLabel } from "@app/components/features"; +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + IconButton, + Input, + Modal, + ModalContent, + ModalTrigger, + Popover, + PopoverContent, + PopoverTrigger, + Tag, + Tooltip +} from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useWorkspace +} from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { + useCreateIdentityProjectAdditionalPrivilege, + useGetIdentityProjectPrivilegeDetails, + useUpdateIdentityProjectAdditionalPrivilege +} from "@app/hooks/api"; +import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/identityProjectAdditionalPrivilege/types"; +import { GeneralPermissionPolicies } from "@app/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies"; +import { NewPermissionRule } from "@app/views/Project/RolePage/components/RolePermissionsSection/components/NewPermissionRule"; +import { PermissionEmptyState } from "@app/views/Project/RolePage/components/RolePermissionsSection/PermissionEmptyState"; +import { + formRolePermission2API, + PROJECT_PERMISSION_OBJECT, + projectRoleFormSchema, + rolePermission2Form +} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils"; +import { renderConditionalComponents } from "@app/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection"; + +type Props = { + privilegeId?: string; + identityId: string; + onGoBack: () => void; + isDisabled?: boolean; +}; + +export const formSchema = z.object({ + slug: z.string().optional(), + temporaryAccess: z + .discriminatedUnion("isTemporary", [ + z.object({ + isTemporary: z.literal(true), + temporaryRange: z.string().min(1), + temporaryAccessStartTime: z.string().datetime(), + temporaryAccessEndTime: z.string().datetime().nullable().optional() + }), + z.object({ + isTemporary: z.literal(false) + }) + ]) + .default({ isTemporary: false }), + permissions: projectRoleFormSchema.shape.permissions +}); + +type TFormSchema = z.infer; + +export const IdentityProjectAdditionalPrivilegeModifySection = ({ + privilegeId = "", + onGoBack, + identityId, + isDisabled +}: Props) => { + const isCreate = !privilegeId; + const { currentWorkspace } = useWorkspace(); + const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const); + const projectId = currentWorkspace?.id || ""; + const { data: privilegeDetails, isLoading } = useGetIdentityProjectPrivilegeDetails({ + identityId, + projectId, + privilegeId + }); + const { permission } = useProjectPermission(); + const isIdentityEditDisabled = permission.cannot( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Identity + ); + + const form = useForm({ + values: privilegeDetails + ? { + ...privilegeDetails, + permissions: rolePermission2Form(privilegeDetails.permissions), + temporaryAccess: privilegeDetails.isTemporary + ? { + isTemporary: true, + temporaryRange: privilegeDetails.temporaryRange || "", + temporaryAccessEndTime: privilegeDetails.temporaryAccessEndTime || "", + temporaryAccessStartTime: privilegeDetails.temporaryAccessStartTime || "" + } + : { + isTemporary: privilegeDetails.isTemporary + } + } + : undefined, + resolver: zodResolver(formSchema) + }); + + const { + handleSubmit, + formState: { isDirty, isSubmitting } + } = form; + + const { mutateAsync: updateIdentityProjectAdditionalPrivilege } = + useUpdateIdentityProjectAdditionalPrivilege(); + const { mutateAsync: createIdentityProjectAdditionalPrivilege } = + useCreateIdentityProjectAdditionalPrivilege(); + + const onSubmit = async (el: TFormSchema) => { + const accessType = !el.temporaryAccess.isTemporary + ? { role: el.slug, isTemporary: false as const } + : { + role: el.slug, + isTemporary: true as const, + temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative, + temporaryRange: el.temporaryAccess.temporaryRange, + temporaryAccessStartTime: el.temporaryAccess.temporaryAccessStartTime + }; + + try { + if (isCreate) { + await createIdentityProjectAdditionalPrivilege({ + permissions: formRolePermission2API(el.permissions), + identityId, + projectId, + slug: el.slug || undefined, + type: accessType + }); + createNotification({ type: "success", text: "Successfully created privilege" }); + } else { + if (!projectId || !privilegeDetails?.id) return; + await updateIdentityProjectAdditionalPrivilege({ + privilegeId: privilegeDetails.id, + permissions: formRolePermission2API(el.permissions), + projectId, + identityId, + slug: el.slug || undefined, + type: accessType + }); + createNotification({ type: "success", text: "Successfully updated privilege" }); + } + onGoBack(); + } catch (err) { + console.log(err); + createNotification({ type: "error", text: "Failed to update role" }); + } + }; + + const privilegeTemporaryAccess = form.watch("temporaryAccess"); + const isTemporary = privilegeTemporaryAccess?.isTemporary; + const isExpired = + privilegeTemporaryAccess?.isTemporary && + new Date() > new Date(privilegeTemporaryAccess.temporaryAccessEndTime || ""); + let text = "Permanent"; + let toolTipText = "Non expiry access"; + + if (isTemporary) { + if (isExpired) { + text = "Access Expired"; + toolTipText = "Timed Access Expired"; + } else { + text = formatDistance( + new Date(privilegeTemporaryAccess.temporaryAccessEndTime || ""), + new Date() + ); + toolTipText = `Until ${format( + new Date(privilegeTemporaryAccess.temporaryAccessEndTime || ""), + "yyyy-MM-dd hh:mm:ss aaa" + )}`; + } + } + + return ( +
+ +
+
+ + + +

+ Modify Additional Privilege +

+
+
+ {isDirty && ( + + )} +
+ + handlePopUpToggle("createPolicy", isOpen)} + > + + + + + handlePopUpToggle("createPolicy")} /> + + +
+
+
+
+
Overview
+
+
+ ( + + + + )} + /> +
+
+ + +
+ + + +
+
+ +
+
+ Configure timed access +
+ {isExpired && Expired} + ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+ + {isTemporary && ( + + )} +
+
+
+
+
+
+
+
+
Policies
+ {(isCreate || !isLoading) && } + {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( + + {renderConditionalComponents(subject, isDisabled)} + + ))} +
+
+
+ ); +}; diff --git a/frontend/src/views/Project/IdentityDetailsPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx new file mode 100644 index 000000000..b2fecab9e --- /dev/null +++ b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx @@ -0,0 +1,245 @@ +import { faFolder, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format, formatDistance } from "date-fns"; +import { AnimatePresence, motion } from "framer-motion"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + DeleteActionModal, + EmptyState, + IconButton, + Table, + TableContainer, + TableSkeleton, + Tag, + TBody, + Td, + Th, + THead, + Tooltip, + Tr +} from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { useDeleteIdentityProjectAdditionalPrivilege } from "@app/hooks/api"; +import { IdentityMembership } from "@app/hooks/api/identities/types"; +import { useListIdentityProjectPrivileges } from "@app/hooks/api/identityProjectAdditionalPrivilege/queries"; + +import { IdentityProjectAdditionalPrivilegeModifySection } from "./IdentityProjectAdditionalPrivilegeModifySection"; + +type Props = { + identityMembershipDetails: IdentityMembership; +}; + +export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDetails }: Props) => { + const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ + "deletePrivilege", + "modifyPrivilege" + ] as const); + const { permission } = useProjectPermission(); + const identityId = identityMembershipDetails?.identity?.id; + const projectId = identityMembershipDetails?.project?.id; + + const { mutateAsync: deletePrivilege } = useDeleteIdentityProjectAdditionalPrivilege(); + + const { data: userProjectPrivileges, isLoading } = useListIdentityProjectPrivileges({ + identityId: identityMembershipDetails?.identity?.id, + projectId: identityMembershipDetails?.project?.id + }); + + const handlePrivilegeDelete = async () => { + const { id } = popUp?.deletePrivilege?.data as { id: string }; + try { + await deletePrivilege({ + privilegeId: id, + projectId, + identityId + }); + createNotification({ type: "success", text: "Successfully removed the privilege" }); + handlePopUpClose("deletePrivilege"); + } catch (err) { + console.log(err); + createNotification({ type: "error", text: "Failed to delete role" }); + } + }; + + return ( +
+ + {popUp?.modifyPrivilege.isOpen ? ( + + handlePopUpClose("modifyPrivilege")} + identityId={identityId} + privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id} + isDisabled={permission.cannot( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Identity + )} + /> + + ) : ( + +
+

+ Project Additional Privilege +

+ + + {(isAllowed) => ( + { + handlePopUpOpen("modifyPrivilege"); + }} + isDisabled={!isAllowed} + > + + + )} + +
+
+ + + + + + + + + + {isLoading && ( + + )} + {!isLoading && + userProjectPrivileges?.map((privilegeDetails) => { + const isTemporary = privilegeDetails?.isTemporary; + const isExpired = + privilegeDetails.isTemporary && + new Date() > new Date(privilegeDetails.temporaryAccessEndTime || ""); + + let text = "Permanent"; + let toolTipText = "Non expiry access"; + if (privilegeDetails.isTemporary) { + if (isExpired) { + text = "Access Expired"; + toolTipText = "Timed Access Expired"; + } else { + text = formatDistance( + new Date(privilegeDetails.temporaryAccessEndTime || ""), + new Date() + ); + toolTipText = `Until ${format( + new Date(privilegeDetails.temporaryAccessEndTime || ""), + "yyyy-MM-dd hh:mm:ss aaa" + )}`; + } + } + + return ( + { + if (evt.key === "Enter") { + handlePopUpOpen("modifyPrivilege", privilegeDetails); + } + }} + onClick={() => handlePopUpOpen("modifyPrivilege", privilegeDetails)} + > + + + + + ); + })} + +
NameType +
{privilegeDetails.slug} + + + {text} + + + +
+ + {(isAllowed) => ( + { + e.stopPropagation(); + e.preventDefault(); + handlePopUpOpen("deletePrivilege", { + id: privilegeDetails?.id, + slug: privilegeDetails?.slug + }); + }} + > + + + )} + +
+
+ {!isLoading && !userProjectPrivileges?.length && ( + + )} +
+
+ handlePopUpToggle("deletePrivilege", isOpen)} + onDeleteApproved={() => handlePrivilegeDelete()} + /> +
+ )} +
+
+ ); +}; diff --git a/frontend/src/views/Project/IdentityDetailsPage/components/IdentityProjectAdditionalPrivilegeSection/index.tsx b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityProjectAdditionalPrivilegeSection/index.tsx new file mode 100644 index 000000000..94f239c3d --- /dev/null +++ b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityProjectAdditionalPrivilegeSection/index.tsx @@ -0,0 +1 @@ +export { IdentityProjectAdditionalPrivilegeSection } from "./IdentityProjectAdditionalPrivilegeSection"; diff --git a/frontend/src/views/Project/IdentityDetailsPage/components/IdentityRoleDetailsSectiona/IdentityRoleDetailsSection.tsx b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityRoleDetailsSectiona/IdentityRoleDetailsSection.tsx new file mode 100644 index 000000000..d412ae681 --- /dev/null +++ b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityRoleDetailsSectiona/IdentityRoleDetailsSection.tsx @@ -0,0 +1,207 @@ +import { faFolder, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format, formatDistance } from "date-fns"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + DeleteActionModal, + EmptyState, + IconButton, + Modal, + ModalContent, + Table, + TableContainer, + TableSkeleton, + Tag, + TBody, + Td, + Th, + THead, + Tooltip, + Tr +} from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { formatProjectRoleName } from "@app/helpers/roles"; +import { usePopUp } from "@app/hooks"; +import { useUpdateIdentityWorkspaceRole } from "@app/hooks/api"; +import { IdentityMembership } from "@app/hooks/api/identities/types"; +import { TProjectRole } from "@app/hooks/api/roles/types"; + +import { IdentityRoleModify } from "./IdentityRoleModify"; + +type Props = { + identityMembershipDetails: IdentityMembership; + isMembershipDetailsLoading?: boolean; +}; + +export const IdentityRoleDetailsSection = ({ + identityMembershipDetails, + isMembershipDetailsLoading +}: Props) => { + const { currentWorkspace } = useWorkspace(); + const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ + "deleteRole", + "modifyRole" + ] as const); + const { mutateAsync: updateIdentityWorkspaceRole } = useUpdateIdentityWorkspaceRole(); + + const handleRoleDelete = async () => { + const { id } = popUp?.deleteRole?.data as TProjectRole; + try { + const updatedRole = identityMembershipDetails?.roles?.filter((el) => el.id !== id); + await updateIdentityWorkspaceRole({ + workspaceId: currentWorkspace?.id || "", + identityId: identityMembershipDetails.identity.id, + roles: updatedRole + }); + createNotification({ type: "success", text: "Successfully removed the role" }); + handlePopUpClose("deleteRole"); + } catch (err) { + console.log(err); + createNotification({ type: "error", text: "Failed to delete role" }); + } + }; + + return ( +
+
+

Project Roles

+ + {(isAllowed) => ( + { + handlePopUpOpen("modifyRole"); + }} + isDisabled={!isAllowed} + > + + + )} + +
+
+ + + + + + + + + + {isMembershipDetailsLoading && ( + + )} + {!isMembershipDetailsLoading && + identityMembershipDetails?.roles?.map((roleDetails) => { + const isTemporary = roleDetails?.isTemporary; + const isExpired = + roleDetails.isTemporary && + new Date() > new Date(roleDetails.temporaryAccessEndTime || ""); + + let text = "Permanent"; + let toolTipText = "Non expiry access"; + if (roleDetails.isTemporary) { + if (isExpired) { + text = "Access Expired"; + toolTipText = "Timed Access Expired"; + } else { + text = formatDistance( + new Date(roleDetails.temporaryAccessEndTime || ""), + new Date() + ); + toolTipText = `Until ${format( + new Date(roleDetails.temporaryAccessEndTime || ""), + "yyyy-MM-dd hh:mm:ss aaa" + )}`; + } + } + + return ( + + + + + + ); + })} + +
RoleType +
+ {roleDetails.role === "custom" + ? roleDetails.customRoleName + : formatProjectRoleName(roleDetails.role)} + + + + {text} + + + +
+ + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("deleteRole", { + id: roleDetails?.id, + slug: roleDetails?.customRoleName || roleDetails?.role + }); + }} + > + + + )} + +
+
+ {!isMembershipDetailsLoading && !identityMembershipDetails?.roles?.length && ( + + )} +
+
+ handlePopUpToggle("deleteRole", isOpen)} + onDeleteApproved={() => handleRoleDelete()} + /> + handlePopUpToggle("modifyRole", isOpen)} + > + + + + +
+ ); +}; diff --git a/frontend/src/views/Project/IdentityDetailsPage/components/IdentityRoleDetailsSectiona/IdentityRoleModify.tsx b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityRoleDetailsSectiona/IdentityRoleModify.tsx new file mode 100644 index 000000000..a0c629045 --- /dev/null +++ b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityRoleDetailsSectiona/IdentityRoleModify.tsx @@ -0,0 +1,332 @@ +/* eslint-disable no-nested-ternary */ +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faCaretDown, faClock, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { format, formatDistance } from "date-fns"; +import ms from "ms"; +import { twMerge } from "tailwind-merge"; +import { z } from "zod"; + +import { TtlFormLabel } from "@app/components/features"; +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + FormControl, + IconButton, + Input, + Popover, + PopoverContent, + PopoverTrigger, + Select, + SelectItem, + Spinner, + Tag, + Tooltip +} from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useWorkspace +} from "@app/context"; +import { useGetProjectRoles, useUpdateIdentityWorkspaceRole } from "@app/hooks/api"; +import { IdentityMembership } from "@app/hooks/api/identities/types"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; +import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types"; + +const roleFormSchema = z.object({ + roles: z + .object({ + slug: z.string(), + temporaryAccess: z.discriminatedUnion("isTemporary", [ + z.object({ + isTemporary: z.literal(true), + temporaryRange: z.string().min(1), + temporaryAccessStartTime: z.string().datetime(), + temporaryAccessEndTime: z.string().datetime().nullable().optional() + }), + z.object({ + isTemporary: z.literal(false) + }) + ]) + }) + .array() +}); +type TRoleForm = z.infer; + +type Props = { + identityProjectMembership: IdentityMembership; +}; + +export const IdentityRoleModify = ({ identityProjectMembership }: Props) => { + const { currentWorkspace } = useWorkspace(); + const workspaceId = currentWorkspace?.id || ""; + const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId); + const { permission } = useProjectPermission(); + const isIdentityEditDisabled = permission.cannot( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Identity + ); + + const roleForm = useForm({ + resolver: zodResolver(roleFormSchema), + values: { + roles: identityProjectMembership?.roles?.map(({ customRoleSlug, role, ...dto }) => ({ + slug: customRoleSlug || role, + temporaryAccess: dto.isTemporary + ? { + isTemporary: true, + temporaryRange: dto.temporaryRange, + temporaryAccessEndTime: dto.temporaryAccessEndTime, + temporaryAccessStartTime: dto.temporaryAccessStartTime + } + : { + isTemporary: dto.isTemporary + } + })) + } + }); + const selectedRoleList = useFieldArray({ + name: "roles", + control: roleForm.control + }); + + const formRoleField = roleForm.watch("roles"); + + const updateIdentityWorkspaceRole = useUpdateIdentityWorkspaceRole(); + + const handleRoleUpdate = async (data: TRoleForm) => { + if (updateIdentityWorkspaceRole.isLoading) return; + + const sanitizedRoles = data.roles.map((el) => { + const { isTemporary } = el.temporaryAccess; + if (!isTemporary) { + return { role: el.slug, isTemporary: false as const }; + } + return { + role: el.slug, + isTemporary: true as const, + temporaryMode: ProjectUserMembershipTemporaryMode.Relative, + temporaryRange: el.temporaryAccess.temporaryRange, + temporaryAccessStartTime: el.temporaryAccess.temporaryAccessStartTime + }; + }); + + try { + await updateIdentityWorkspaceRole.mutateAsync({ + workspaceId, + identityId: identityProjectMembership.identity.id, + roles: sanitizedRoles + }); + createNotification({ text: "Successfully updated roles", type: "success" }); + } catch (err) { + createNotification({ text: "Failed to update role", type: "error" }); + } + }; + + if (isRolesLoading) + return ( +
+ +
+ ); + + return ( +
+
+ {selectedRoleList.fields.map(({ id }, index) => { + const { temporaryAccess } = formRoleField[index]; + const isTemporary = temporaryAccess?.isTemporary; + const isExpired = + temporaryAccess.isTemporary && + new Date() > new Date(temporaryAccess.temporaryAccessEndTime || ""); + + return ( +
+ ( + + )} + /> + + +
+ + + +
+
+ +
+
+ Configure timed access +
+ {isExpired && Expired} + ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+ + {temporaryAccess.isTemporary && ( + + )} +
+
+
+
+ { + if (selectedRoleList.fields.length > 1) { + selectedRoleList.remove(index); + } + }} + > + + +
+ ); + })} +
+
+ + {(isAllowed) => ( + + )} + + +
+
+ ); +}; diff --git a/frontend/src/views/Project/IdentityDetailsPage/components/IdentityRoleDetailsSectiona/index.tsx b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityRoleDetailsSectiona/index.tsx new file mode 100644 index 000000000..45a444e73 --- /dev/null +++ b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityRoleDetailsSectiona/index.tsx @@ -0,0 +1 @@ +export { IdentityRoleDetailsSection } from "./IdentityRoleDetailsSection"; diff --git a/frontend/src/views/Project/IdentityDetailsPage/index.tsx b/frontend/src/views/Project/IdentityDetailsPage/index.tsx new file mode 100644 index 000000000..b5b091a61 --- /dev/null +++ b/frontend/src/views/Project/IdentityDetailsPage/index.tsx @@ -0,0 +1 @@ +export { IdentityDetailsPage } from "./IdentityDetailPage"; diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx index 49f8e3f9b..40c04f786 100644 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; +import { useRouter } from "next/router"; import { faArrowDown, faArrowUp, @@ -65,6 +66,7 @@ const formatRoleName = (role: string, customRoleName?: string) => { export const IdentityTab = withProjectPermission( () => { const { currentWorkspace } = useWorkspace(); + const router = useRouter(); const { offset, @@ -239,7 +241,18 @@ export const IdentityTab = withProjectPermission( createdAt } = identityMember; return ( - + { + if (evt.key === "Enter") { + router.push(`/project/${workspaceId}/identities/${id}`); + } + }} + onClick={() => router.push(`/project/${workspaceId}/identities/${id}`)} + > {name} @@ -340,9 +353,11 @@ export const IdentityTab = withProjectPermission( size="sm" variant="plain" ariaLabel="update-role" - onClick={() => - handlePopUpOpen("updateRole", { ...identityMember, index }) - } + onClick={(evt) => { + evt.stopPropagation(); + evt.preventDefault(); + handlePopUpOpen("updateRole", { ...identityMember, index }); + }} > @@ -357,7 +372,9 @@ export const IdentityTab = withProjectPermission( > {(isAllowed) => ( { + onClick={(evt) => { + evt.stopPropagation(); + evt.preventDefault(); handlePopUpOpen("deleteIdentity", { identityId: id, name diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRoleForm.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRoleForm.tsx index e354f1015..2271f5255 100644 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRoleForm.tsx +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRoleForm.tsx @@ -1,20 +1,37 @@ +import Link from "next/link"; + +import { Alert, AlertDescription } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; import { IdentityMembership } from "@app/hooks/api/identities/types"; import { IdentityRbacSection } from "./IdentityRbacSection"; -import { SpecificPrivilegeSection } from "./SpecificPrivilegeSection"; type Props = { identityProjectMember: IdentityMembership; onOpenUpgradeModal: (title: string) => void; }; export const IdentityRoleForm = ({ identityProjectMember, onOpenUpgradeModal }: Props) => { + const { currentWorkspace } = useWorkspace(); + return (
- + + + Additional privileges now offer full permissions and have been moved to a new screen. +
+ + Click here to access them. + +
+
); }; diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/SpecificPrivilegeSection.tsx deleted file mode 100644 index 7f76992bb..000000000 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/SpecificPrivilegeSection.tsx +++ /dev/null @@ -1,535 +0,0 @@ -import { Controller, useForm } from "react-hook-form"; -import { - faArrowRotateLeft, - faCaretDown, - faCheck, - faClock, - faPlus, - faTrash -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { format, formatDistance } from "date-fns"; -import ms from "ms"; -import { twMerge } from "tailwind-merge"; -import { z } from "zod"; - -import { TtlFormLabel } from "@app/components/features"; -import { createNotification } from "@app/components/notifications"; -import { ProjectPermissionCan } from "@app/components/permissions"; -import { - Button, - Checkbox, - DeleteActionModal, - FormControl, - FormLabel, - IconButton, - Input, - Popover, - PopoverContent, - PopoverTrigger, - Select, - SelectItem, - Spinner, - Tag, - Tooltip -} from "@app/components/v2"; -import { SecretPathInput } from "@app/components/v2/SecretPathInput"; -import { - ProjectPermissionActions, - ProjectPermissionSub, - useProjectPermission, - useWorkspace -} from "@app/context"; -import { usePopUp } from "@app/hooks"; -import { - TProjectUserPrivilege, - useCreateIdentityProjectAdditionalPrivilege, - useDeleteIdentityProjectAdditionalPrivilege, - useUpdateIdentityProjectAdditionalPrivilege -} from "@app/hooks/api"; -import { useListIdentityProjectPrivileges } from "@app/hooks/api/identityProjectAdditionalPrivilege/queries"; - -const secretPermissionSchema = z.object({ - secretPath: z.string().optional(), - environmentSlug: z.string(), - [ProjectPermissionActions.Edit]: z.boolean().optional(), - [ProjectPermissionActions.Read]: z.boolean().optional(), - [ProjectPermissionActions.Create]: z.boolean().optional(), - [ProjectPermissionActions.Delete]: z.boolean().optional(), - temporaryAccess: z.discriminatedUnion("isTemporary", [ - z.object({ - isTemporary: z.literal(true), - temporaryRange: z.string().min(1), - temporaryAccessStartTime: z.string().datetime(), - temporaryAccessEndTime: z.string().datetime().nullable().optional() - }), - z.object({ - isTemporary: z.literal(false) - }) - ]) -}); -type TSecretPermissionForm = z.infer; -const SpecificPrivilegeSecretForm = ({ - privilege, - identityId -}: { - privilege: TProjectUserPrivilege; - identityId: string; -}) => { - const { currentWorkspace } = useWorkspace(); - const projectSlug = currentWorkspace?.slug || ""; - - const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ - "deletePrivilege" - ] as const); - const { permission } = useProjectPermission(); - const isMemberEditDisabled = permission.cannot( - ProjectPermissionActions.Edit, - ProjectPermissionSub.Identity - ); - - const updateIdentityPrivilege = useUpdateIdentityProjectAdditionalPrivilege(); - const deleteIdentityPrivilege = useDeleteIdentityProjectAdditionalPrivilege(); - - const privilegeForm = useForm({ - resolver: zodResolver(secretPermissionSchema), - values: { - environmentSlug: privilege.permissions?.[0]?.conditions?.environment, - // secret path will be inside $glob operator - secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob || "", - read: privilege.permissions?.some(({ action }) => - action.includes(ProjectPermissionActions.Read) - ), - edit: privilege.permissions?.some(({ action }) => - action.includes(ProjectPermissionActions.Edit) - ), - create: privilege.permissions?.some(({ action }) => - action.includes(ProjectPermissionActions.Create) - ), - delete: privilege.permissions?.some(({ action }) => - action.includes(ProjectPermissionActions.Delete) - ), - // zod will pick it - temporaryAccess: privilege - } - }); - - const temporaryAccessField = privilegeForm.watch("temporaryAccess"); - const selectedEnvironmentSlug = privilegeForm.watch("environmentSlug"); - const isTemporary = temporaryAccessField?.isTemporary; - const isExpired = - temporaryAccessField.isTemporary && - new Date() > new Date(temporaryAccessField.temporaryAccessEndTime || ""); - - const handleUpdatePrivilege = async (data: TSecretPermissionForm) => { - if (updateIdentityPrivilege.isLoading) return; - try { - const actions = [ - { action: ProjectPermissionActions.Read, allowed: data.read }, - { action: ProjectPermissionActions.Create, allowed: data.create }, - { action: ProjectPermissionActions.Delete, allowed: data.delete }, - { action: ProjectPermissionActions.Edit, allowed: data.edit } - ]; - await updateIdentityPrivilege.mutateAsync({ - privilegeDetails: { - ...data.temporaryAccess, - privilegePermission: { - actions: actions.filter(({ allowed }) => allowed).map(({ action }) => action), - subject: ProjectPermissionSub.Secrets, - conditions: { - environment: data.environmentSlug, - ...(data.secretPath ? { secretPath: { $glob: data.secretPath } } : {}) - } - } - }, - privilegeSlug: privilege.slug, - identityId, - projectSlug - }); - createNotification({ - type: "success", - text: "Successfully updated privilege" - }); - } catch (err) { - createNotification({ - type: "error", - text: "Failed to update privilege" - }); - } - }; - - const handleDeletePrivilege = async () => { - if (deleteIdentityPrivilege.isLoading) return; - try { - await deleteIdentityPrivilege.mutateAsync({ - identityId, - privilegeSlug: privilege.slug, - projectSlug - }); - createNotification({ - type: "success", - text: "Successfully deleted privilege" - }); - } catch (err) { - createNotification({ - type: "error", - text: "Failed to delete privilege" - }); - } - }; - - const getAccessLabel = (exactTime = false) => { - if (isExpired) return "Access expired"; - if (!temporaryAccessField?.isTemporary) return "Permanent"; - if (exactTime) - return `Until ${format( - new Date(temporaryAccessField.temporaryAccessEndTime || ""), - "yyyy-MM-dd HH:mm:ss" - )}`; - return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); - }; - - return ( -
-
-
- ( - - - - )} - /> - ( - - - - )} - /> -
- ( -
- - field.onChange(isChecked)} - /> -
- )} - /> - ( -
- - field.onChange(isChecked)} - /> -
- )} - /> - ( -
- - field.onChange(isChecked)} - /> -
- )} - /> - ( -
- - field.onChange(isChecked)} - /> -
- )} - /> -
-
- - -
- - - -
-
- -
-
- Configure timed access -
- {isExpired && Expired} - ( - } - isError={Boolean(error?.message)} - errorText={error?.message} - > - - - )} - /> -
- - {temporaryAccessField.isTemporary && ( - - )} -
-
-
-
- {privilegeForm.formState.isDirty ? ( - <> - - privilegeForm.reset()} - > - - - - - - {privilegeForm.formState.isSubmitting ? ( - - ) : ( - - )} - - - - ) : ( - - handlePopUpOpen("deletePrivilege")} - > - - - - )} -
-
-
- handlePopUpToggle("deletePrivilege", isOpen)} - deleteKey="delete" - onClose={() => handlePopUpClose("deletePrivilege")} - onDeleteApproved={handleDeletePrivilege} - /> -
- ); -}; - -type Props = { - identityId: string; -}; - -export const SpecificPrivilegeSection = ({ identityId }: Props) => { - const { currentWorkspace } = useWorkspace(); - const projectSlug = currentWorkspace?.slug || ""; - const { data: identityPrivileges, isLoading } = useListIdentityProjectPrivileges({ - identityId, - projectSlug - }); - - const createIdentityPrivilege = useCreateIdentityProjectAdditionalPrivilege(); - - const handleCreatePrivilege = async () => { - if (createIdentityPrivilege.isLoading) return; - try { - await createIdentityPrivilege.mutateAsync({ - privilegePermission: { - actions: [ProjectPermissionActions.Read], - subject: ProjectPermissionSub.Secrets, - conditions: { - environment: currentWorkspace?.environments?.[0].slug as string - } - }, - identityId, - projectSlug - }); - createNotification({ - type: "success", - text: "Successfully created privilege" - }); - } catch (err) { - createNotification({ - type: "error", - text: "Failed to create privilege" - }); - } - }; - - return ( -
-
- Additional Privileges - {isLoading && } -
-

- Select individual privileges to associate with the identity. -

-
- {identityPrivileges - ?.filter(({ permissions }) => - permissions?.[0]?.subject?.includes(ProjectPermissionSub.Secrets) - ) - .sort((a, b) => a.id.localeCompare(b.id)) - ?.map((privilege) => ( - - ))} -
- - {(isAllowed) => ( - - )} - -
- ); -}; diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx index 51581de4d..a5a7dc74d 100644 --- a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -6,7 +6,6 @@ import { faCheck, faClock, faLockOpen, - faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -18,7 +17,6 @@ import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; -import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, Checkbox, @@ -48,10 +46,7 @@ import { usePopUp } from "@app/hooks"; import { TProjectUserPrivilege, useCreateAccessRequest, - useCreateProjectUserAdditionalPrivilege, - useDeleteProjectUserAdditionalPrivilege, - useListProjectUserPrivileges, - useUpdateProjectUserAdditionalPrivilege + useDeleteProjectUserAdditionalPrivilege } from "@app/hooks/api"; import { TAccessApprovalPolicy } from "@app/hooks/api/types"; @@ -94,7 +89,6 @@ export const SpecificPrivilegeSecretForm = ({ const isMemberEditDisabled = permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Member) && !!privilege; - const updateUserPrivilege = useUpdateProjectUserAdditionalPrivilege(); const deleteUserPrivilege = useDeleteProjectUserAdditionalPrivilege(); const requestAccess = useCreateAccessRequest(); @@ -165,53 +159,6 @@ export const SpecificPrivilegeSecretForm = ({ temporaryAccessField.isTemporary && new Date() > new Date(temporaryAccessField.temporaryAccessEndTime || ""); - const handleUpdatePrivilege = async (data: TSecretPermissionForm) => { - if (!privilege) { - createNotification({ - type: "error", - text: "No privilege to update found.", - title: "Error" - }); - - return; - } - - if (updateUserPrivilege.isLoading) return; - try { - const actions = [ - { action: ProjectPermissionActions.Read, allowed: data.read }, - { action: ProjectPermissionActions.Create, allowed: data.create }, - { action: ProjectPermissionActions.Delete, allowed: data.delete }, - { action: ProjectPermissionActions.Edit, allowed: data.edit } - ]; - const conditions: { environment: string; secretPath?: { $glob: string } } = { - environment: data.environmentSlug - }; - if (data.secretPath) { - conditions.secretPath = { $glob: removeTrailingSlash(data.secretPath) }; - } - await updateUserPrivilege.mutateAsync({ - privilegeId: privilege.id, - ...data.temporaryAccess, - permissions: { - subject: ProjectPermissionSub.Secrets, - conditions, - actions: actions.filter((i) => i.allowed).map((i) => i.action) - }, - projectMembershipId: privilege.projectMembershipId - }); - createNotification({ - type: "success", - text: "Successfully updated privilege" - }); - } catch (err) { - createNotification({ - type: "error", - text: "Failed to update privilege" - }); - } - }; - const handleDeletePrivilege = async () => { if (!privilege) { createNotification({ @@ -296,11 +243,7 @@ export const SpecificPrivilegeSecretForm = ({ }; const handleSubmit = async (data: TSecretPermissionForm) => { - if (privilege) { - handleUpdatePrivilege(data); - } else { - handleRequestAccess(data); - } + handleRequestAccess(data); }; const getAccessLabel = (exactTime = false) => { @@ -627,78 +570,3 @@ export const SpecificPrivilegeSecretForm = ({ ); }; - -type Props = { - membershipId: string; -}; - -export const SpecificPrivilegeSection = ({ membershipId }: Props) => { - const { data: userPrivileges, isLoading } = useListProjectUserPrivileges(membershipId); - const { currentWorkspace } = useWorkspace(); - - const createUserPrivilege = useCreateProjectUserAdditionalPrivilege(); - - const handleCreatePrivilege = async () => { - if (createUserPrivilege.isLoading) return; - try { - await createUserPrivilege.mutateAsync({ - permissions: { - actions: [ProjectPermissionActions.Read], - subject: ProjectPermissionSub.Secrets, - conditions: { - environment: currentWorkspace?.environments?.[0].slug || "" - } - }, - projectMembershipId: membershipId - }); - createNotification({ - type: "success", - text: "Successfully created privilege" - }); - } catch (err) { - createNotification({ - type: "error", - text: "Failed to create privilege" - }); - } - }; - - return ( -
-
- Additional Privileges - {isLoading && } -
-

- Select individual privileges to associate with the user. -

-
- {userPrivileges - ?.filter(({ permissions }) => - permissions?.[0]?.subject?.includes(ProjectPermissionSub.Secrets) - ) - .sort((a, b) => a.id.localeCompare(b.id)) - ?.map((privilege) => ( - - ))} -
- - {(isAllowed) => ( - - )} - -
- ); -}; diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx index a8d9fcfc7..8f8b784a5 100644 --- a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx @@ -115,7 +115,7 @@ export const MembersTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Prop return ( {