Fix framework pages permissions handling

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-31 17:49:56 +01:00
committed by Bryan Frimin
parent 269f963b48
commit 095254a73d
11 changed files with 663 additions and 188 deletions

View File

@@ -29,8 +29,6 @@ import { promisifyMutation } from "@probo/helpers";
import type { FrameworkGraphControlNodeQuery } from "/__generated__/core/FrameworkGraphControlNodeQuery.graphql";
import { frameworkControlNodeQuery } from "/hooks/graph/FrameworkGraph";
import type { FrameworkDetailPageFragment$data } from "/__generated__/core/FrameworkDetailPageFragment.graphql";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const attachStateOfApplicabilityMutation = graphql`
mutation FrameworkControlPageAttachStateOfApplicabilityMutation(
@@ -227,11 +225,6 @@ export default function FrameworkControlPage({ queryRef }: Props) {
const organizationId = useOrganizationId();
const confirm = useConfirm();
const navigate = useNavigate();
const { isAuthorized } = use(PermissionsContext);
const [detachStateOfApplicability, isDetachingStateOfApplicability] =
useMutation(detachStateOfApplicabilityMutation);
const [attachStateOfApplicability, isAttachingStateOfApplicability] =
useMutation(attachStateOfApplicabilityMutation);
const [detachMeasure, isDetachingMeasure] = useMutation(
detachMeasureMutation,
);
@@ -253,6 +246,11 @@ export default function FrameworkControlPage({ queryRef }: Props) {
attachSnapshotMutation,
);
const [deleteControl] = useMutation(deleteControlMutation);
const [attachStateOfApplicability, isAttachingStateOfApplicability] =
useMutation(attachStateOfApplicabilityMutation);
const [detachStateOfApplicability, isDetachingStateOfApplicability] =
useMutation(detachStateOfApplicabilityMutation);
const [attachObligation, isAttachingObligation] = useMutation(
attachObligationMutation,
);
@@ -260,61 +258,33 @@ export default function FrameworkControlPage({ queryRef }: Props) {
detachObligationMutation,
);
const canLinkStateOfApplicability = isAuthorized(
"Control",
"createStateOfApplicabilityControlMapping",
);
const canUnlinkStateOfApplicability = isAuthorized(
"Control",
"deleteStateOfApplicabilityControlMapping",
);
const canLinkStateOfApplicability =
control.canCreateStateOfApplicabilityMapping;
const canUnlinkStateOfApplicability =
control.canDeleteStateOfApplicabilityMapping;
const statesOfApplicabilityReadOnly =
!canLinkStateOfApplicability && !canUnlinkStateOfApplicability;
const canLinkMeasure = isAuthorized(
"Control",
"createControlMeasureMapping",
);
const canUnlinkMeasure = isAuthorized(
"Control",
"deleteControlMeasureMapping",
);
const canLinkMeasure = control.canCreateMeasureMapping;
const canUnlinkMeasure = control.canDeleteMeasureMapping;
const measuresReadOnly = !canLinkMeasure && !canUnlinkMeasure;
const canLinkDocument = isAuthorized(
"Control",
"createControlDocumentMapping",
);
const canUnlinkDocument = isAuthorized(
"Control",
"deleteControlDocumentMapping",
);
const canLinkDocument = control.canCreateDocumentMapping;
const canUnlinkDocument = control.canDeleteDocumentMapping;
const documentsReadOnly = !canLinkDocument && !canUnlinkDocument;
const canLinkAudit = isAuthorized("Control", "createControlAuditMapping");
const canUnlinkAudit = isAuthorized("Control", "deleteControlAuditMapping");
const canLinkAudit = control.canCreateAuditMapping;
const canUnlinkAudit = control.canDeleteAuditMapping;
const auditsReadOnly = !canLinkAudit && !canUnlinkAudit;
const canLinkObligation = isAuthorized(
"Control",
"createControlObligationMapping",
);
const canUnlinkObligation = isAuthorized(
"Control",
"deleteControlObligationMapping",
);
const obligationsReadOnly = !canLinkObligation && !canUnlinkObligation;
const canLinkSnapshot = isAuthorized(
"Control",
"createControlSnapshotMapping",
);
const canUnlinkSnapshot = isAuthorized(
"Control",
"deleteControlSnapshotMapping",
);
const canLinkSnapshot = control.canCreateSnapshotMapping;
const canUnlinkSnapshot = control.canDeleteSnapshotMapping;
const snapshotsReadOnly = !canLinkSnapshot && !canUnlinkSnapshot;
const canLinkObligation = control.canCreateObligationMapping;
const canUnlinkObligation = control.canDeleteObligationMapping;
const obligationsReadOnly = !canLinkObligation && !canUnlinkObligation;
const withErrorHandling =
<T extends MutationParameters>(
mutationFn: (config: UseMutationConfig<T>) => void,
@@ -382,7 +352,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
</div>
</div>
<div className="flex gap-2">
{isAuthorized("Control", "updateControl") && (
{control.canUpdate && (
<FrameworkControlDialog
frameworkId={framework.id}
connectionId={connectionId}
@@ -393,7 +363,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
</Button>
</FrameworkControlDialog>
)}
{isAuthorized("Control", "deleteControl") && (
{control.canDelete && (
<ActionDropdown variant="secondary">
<DropdownItem
icon={IconTrashCan}

View File

@@ -31,8 +31,6 @@ import type { FrameworkDetailPageExportFrameworkMutation } from "/__generated__/
import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog";
import { FrameworkControlDialog } from "./dialogs/FrameworkControlDialog";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const frameworkDetailFragment = graphql`
fragment FrameworkDetailPageFragment on Framework {
@@ -41,6 +39,10 @@ const frameworkDetailFragment = graphql`
description
lightLogoURL
darkLogoURL
canExport: permission(action: "core:franework:export")
canUpdate: permission(action: "core:framework:update")
canDelete: permission(action: "core:framework:delete")
canCreateControl: permission(action: "core:control:create")
organization {
name
}
@@ -88,10 +90,15 @@ type Props = {
};
export default function FrameworkDetailPage(props: Props) {
const { queryRef } = props;
const { __ } = useTranslate();
const { controlId } = useParams<{ controlId?: string }>();
const organizationId = useOrganizationId();
const data = usePreloadedQuery(frameworkNodeQuery, props.queryRef);
const data = usePreloadedQuery<FrameworkGraphNodeQuery>(
frameworkNodeQuery,
queryRef,
);
const framework = useFragment<FrameworkDetailPageFragment$key>(
frameworkDetailFragment,
data.node,
@@ -106,7 +113,6 @@ export default function FrameworkDetailPage(props: Props) {
framework,
ConnectionHandler.getConnectionID(organizationId, connectionListKey)!,
);
const { isAuthorized } = use(PermissionsContext);
const [generateFrameworkStateOfApplicability] =
useMutationWithToasts<FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation>(
generateFrameworkStateOfApplicabilityMutation,
@@ -155,7 +161,7 @@ export default function FrameworkDetailPage(props: Props) {
</>
}
>
{isAuthorized("Framework", "updateFramework") && (
{framework.canUpdate && (
<FrameworkFormDialog
organizationId={organizationId}
framework={framework}
@@ -202,7 +208,7 @@ export default function FrameworkDetailPage(props: Props) {
>
{__("Export Framework")}
</DropdownItem>
{isAuthorized("Framework", "deleteFramework") && (
{framework.canDelete && (
<DropdownItem
icon={IconTrashCan}
variant="danger"
@@ -231,7 +237,7 @@ export default function FrameworkDetailPage(props: Props) {
active={selectedControl?.id === control.id}
/>
))}
{isAuthorized("Organization", "createControl") && (
{framework.canCreateControl && (
<FrameworkControlDialog
frameworkId={framework.id}
connectionId={connectionId}

View File

@@ -27,9 +27,8 @@ import {
import { Link } from "react-router";
import type { FrameworksPageCardFragment$key } from "/__generated__/core/FrameworksPageCardFragment.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { useState, type ChangeEventHandler, use } from "react";
import { useState, type ChangeEventHandler } from "react";
import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = {
queryRef: PreloadedQuery<FrameworkGraphListQuery>;
@@ -53,7 +52,6 @@ const importFrameworkMutation = graphql`
export default function FrameworksPage(props: Props) {
const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
usePageTitle(__("Frameworks"));
const data = usePreloadedQuery(frameworksQuery, props.queryRef);
const connectionId = data.organization.frameworks!.__id;
@@ -120,9 +118,9 @@ export default function FrameworksPage(props: Props) {
const isLoading = isUploading || isImporting;
const hasAnyAction =
isAuthorized("Framework", "updateFramework") ||
isAuthorized("Framework", "deleteFramework");
const hasAnyAction = frameworks.some(
({ canUpdate, canDelete }) => canUpdate || canDelete,
);
return (
<div className="space-y-6">
@@ -135,7 +133,7 @@ export default function FrameworksPage(props: Props) {
title={__("Frameworks")}
description={__("Manage your compliance frameworks")}
>
{isAuthorized("Organization", "createFramework") && (
{data.organization.canCreateFramework && (
<>
<FileButton
variant="secondary"
@@ -175,6 +173,8 @@ const frameworkCardFragment = graphql`
description
lightLogoURL
darkLogoURL
canUpdate: permission(action: "core:framework:update")
canDelete: permission(action: "core:framework:delete")
}
`;
@@ -187,7 +187,6 @@ type FrameworkCardProps = {
function FrameworkCard(props: FrameworkCardProps) {
const framework = useFragment(frameworkCardFragment, props.framework);
const { isAuthorized } = use(PermissionsContext);
const deleteFramework = useDeleteFrameworkMutation(
framework,
props.connectionId,
@@ -210,7 +209,7 @@ function FrameworkCard(props: FrameworkCardProps) {
/>
{props.hasAnyAction && (
<ActionDropdown className="z-10 relative">
{isAuthorized("Framework", "updateFramework") && (
{framework.canUpdate && (
<DropdownItem
icon={IconPencil}
onClick={() => {
@@ -220,7 +219,7 @@ function FrameworkCard(props: FrameworkCardProps) {
{__("Edit")}
</DropdownItem>
)}
{isAuthorized("Framework", "deleteFramework") && (
{framework.canDelete && (
<DropdownItem
icon={IconTrashCan}
onClick={() => deleteFramework()}