Skip to content

Commit

Permalink
refact: add PermissionsGate + debug state/log for forbidden errors
Browse files Browse the repository at this point in the history
  • Loading branch information
davidlougheed committed Feb 13, 2025
1 parent f2f9265 commit 18a4f7c
Show file tree
Hide file tree
Showing 12 changed files with 322 additions and 275 deletions.
6 changes: 3 additions & 3 deletions src/components/DataExplorerContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ const DataExplorerContent = () => {
document.title = `${SITE_NAME} - Explore Your Data`;
}, []);

const { hasPermission: canQueryData, hasAttempted: hasAttemptedQueryPermissions } =
useCanQueryAtLeastOneProjectOrDataset();
const perms = useCanQueryAtLeastOneProjectOrDataset();
const { hasPermission: canQueryData, hasAttempted: hasAttemptedQueryPermissions } = perms;

if (hasAttemptedQueryPermissions && !canQueryData) {
return <ForbiddenContent message="You do not have permission to query any data." />;
return <ForbiddenContent message="You do not have permission to query any data." debugState={perms} />;
}

return (
Expand Down
25 changes: 17 additions & 8 deletions src/components/ForbiddenContent.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import type { ReactNode } from "react";
import { useEffect } from "react";
import { Layout, Result } from "antd";
import { LAYOUT_CONTENT_STYLE } from "@/styles/layoutContent";

export type ForbiddenContentProps = { message?: ReactNode };
export type ForbiddenContentProps = { message?: ReactNode; debugState?: object };

const DEFAULT_MESSAGE = "You do not have permission to view this content.";

const ForbiddenContent = ({ message }: ForbiddenContentProps) => (
<Layout>
<Layout.Content style={LAYOUT_CONTENT_STYLE}>
<Result status="error" title="Forbidden" subTitle={message ?? DEFAULT_MESSAGE} />
</Layout.Content>
</Layout>
);
const ForbiddenContent = ({ message, debugState }: ForbiddenContentProps) => {
useEffect(() => {
if (debugState) {
console.debug("ForbiddenContent debug state:", debugState);
}
}, [debugState]);

return (
<Layout>
<Layout.Content style={LAYOUT_CONTENT_STYLE}>
<Result status="error" title="Forbidden" subTitle={message ?? DEFAULT_MESSAGE} />
</Layout.Content>
</Layout>
);
};

export default ForbiddenContent;
24 changes: 24 additions & 0 deletions src/components/PermissionsGate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { ReactNode } from "react";
import type { Resource } from "bento-auth-js";
import { useResourcePermissionsWrapper } from "@/hooks";
import ForbiddenContent from "@/components/ForbiddenContent";

type PermissionsGateProps = {
resource: Resource;
children: ReactNode;
requiredPermissions: string[];
forbiddenMessage?: string;
};

const PermissionsGate = ({ resource, children, requiredPermissions, forbiddenMessage }: PermissionsGateProps) => {
const rp = useResourcePermissionsWrapper(resource);
const { permissions, hasAttemptedPermissions } = rp;

if (hasAttemptedPermissions && !requiredPermissions.every((reqPerm) => permissions.includes(reqPerm))) {
return <ForbiddenContent message={forbiddenMessage} debugState={rp} />;
}

return children;
};

export default PermissionsGate;
29 changes: 14 additions & 15 deletions src/components/manager/ManagerAnalysisContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,34 @@ import { useNavigate } from "react-router-dom";

import { analyzeData, RESOURCE_EVERYTHING } from "bento-auth-js";

import { useResourcePermissionsWrapper } from "@/hooks";
import { submitAnalysisWorkflowRun } from "@/modules/wes/actions";
import { useAppDispatch } from "@/store";

import ForbiddenContent from "../ForbiddenContent";
import PermissionsGate from "@/components/PermissionsGate";
import RunSetupWizard from "./RunSetupWizard";
import RunSetupConfirmDisplay from "./RunSetupConfirmDisplay";

const ManagerAnalysisContent = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();

const { permissions, hasAttemptedPermissions } = useResourcePermissionsWrapper(RESOURCE_EVERYTHING);

// TODO: each workflow should have definitions for permissions scopes, so we can instead check if we can run at
// least one workflow.

if (hasAttemptedPermissions && !permissions.includes(analyzeData)) {
return <ForbiddenContent message="You do not have permission to view the analysis wizard." />;
}

return (
<RunSetupWizard
workflowType="analysis"
confirmDisplay={(props) => <RunSetupConfirmDisplay runButtonText="Run Analysis" {...props} />}
onSubmit={({ selectedWorkflow, inputs }) => {
dispatch(submitAnalysisWorkflowRun(selectedWorkflow, inputs, "/data/manager/runs", navigate));
}}
/>
<PermissionsGate
requiredPermissions={[analyzeData]}
resource={RESOURCE_EVERYTHING}
forbiddenMessage="You do not have permission to view the analysis wizard."
>
<RunSetupWizard
workflowType="analysis"
confirmDisplay={(props) => <RunSetupConfirmDisplay runButtonText="Run Analysis" {...props} />}
onSubmit={({ selectedWorkflow, inputs }) => {
dispatch(submitAnalysisWorkflowRun(selectedWorkflow, inputs, "/data/manager/runs", navigate));
}}
/>
</PermissionsGate>
);
};

Expand Down
29 changes: 14 additions & 15 deletions src/components/manager/ManagerExportContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,34 @@ import { useNavigate } from "react-router-dom";

import { exportData, RESOURCE_EVERYTHING } from "bento-auth-js";

import { useResourcePermissionsWrapper } from "@/hooks";
import { submitExportWorkflowRun } from "@/modules/wes/actions";
import { useAppDispatch } from "@/store";

import ForbiddenContent from "../ForbiddenContent";
import PermissionsGate from "@/components/PermissionsGate";
import RunSetupWizard from "./RunSetupWizard";
import RunSetupConfirmDisplay from "./RunSetupConfirmDisplay";

const ManagerExportContent = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();

const { permissions, hasAttemptedPermissions } = useResourcePermissionsWrapper(RESOURCE_EVERYTHING);

// TODO: each workflow should have definitions for permissions scopes, so we can instead check if we can run at
// least one workflow.

if (hasAttemptedPermissions && !permissions.includes(exportData)) {
return <ForbiddenContent message="You do not have permission to view the export wizard." />;
}

return (
<RunSetupWizard
workflowType="export"
confirmDisplay={(props) => <RunSetupConfirmDisplay runButtonText="Run Export" {...props} />}
onSubmit={({ selectedWorkflow, inputs }) => {
dispatch(submitExportWorkflowRun(selectedWorkflow, inputs, "/data/manager/runs", navigate));
}}
/>
<PermissionsGate
resource={RESOURCE_EVERYTHING}
requiredPermissions={[exportData]}
forbiddenMessage="You do not have permission to view the export wizard."
>
<RunSetupWizard
workflowType="export"
confirmDisplay={(props) => <RunSetupConfirmDisplay runButtonText="Run Export" {...props} />}
onSubmit={({ selectedWorkflow, inputs }) => {
dispatch(submitExportWorkflowRun(selectedWorkflow, inputs, "/data/manager/runs", navigate));
}}
/>
</PermissionsGate>
);
};

Expand Down
33 changes: 16 additions & 17 deletions src/components/manager/ManagerIngestionContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,35 @@ import { useNavigate } from "react-router-dom";

import { ingestData, ingestReferenceMaterial, RESOURCE_EVERYTHING } from "bento-auth-js";

import { useResourcePermissionsWrapper } from "@/hooks";
import { submitIngestionWorkflowRun } from "@/modules/wes/actions";
import { useAppDispatch } from "@/store";

import ForbiddenContent from "../ForbiddenContent";
import PermissionsGate from "@/components/PermissionsGate";
import RunSetupWizard from "./RunSetupWizard";
import RunSetupConfirmDisplay from "./RunSetupConfirmDisplay";
import { useAppDispatch } from "@/store";

const ManagerIngestionContent = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();

const { permissions, hasAttemptedPermissions } = useResourcePermissionsWrapper(RESOURCE_EVERYTHING);

// TODO: each workflow should have definitions for permissions scopes, so we can instead check if we can run at
// least one workflow.

if (hasAttemptedPermissions && !(permissions.includes(ingestData) || permissions.includes(ingestReferenceMaterial))) {
return <ForbiddenContent message="You do not have permission to view the ingestion wizard." />;
}

return (
<RunSetupWizard
workflowType="ingestion"
workflowSelectionDescription="Choose an ingestion workflow."
confirmDisplay={(props) => <RunSetupConfirmDisplay runButtonText="Run Ingestion" {...props} />}
onSubmit={({ selectedWorkflow, inputs }) => {
dispatch(submitIngestionWorkflowRun(selectedWorkflow, inputs, "/data/manager/runs", navigate));
}}
/>
<PermissionsGate
resource={RESOURCE_EVERYTHING}
requiredPermissions={[ingestData, ingestReferenceMaterial]}
forbiddenMessage="You do not have permission to view the ingestion wizard."
>
<RunSetupWizard
workflowType="ingestion"
workflowSelectionDescription="Choose an ingestion workflow."
confirmDisplay={(props) => <RunSetupConfirmDisplay runButtonText="Run Ingestion" {...props} />}
onSubmit={({ selectedWorkflow, inputs }) => {
dispatch(submitIngestionWorkflowRun(selectedWorkflow, inputs, "/data/manager/runs", navigate));
}}
/>
</PermissionsGate>
);
};

Expand Down
5 changes: 3 additions & 2 deletions src/components/manager/access/AccessTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const AccessTabs = () => {
const navigate = useNavigate();
const { tab } = useParams();

const { hasAtLeastOneViewPermissionsGrant, hasAttempted: hasAttemptedPermissions } = useAuthzManagementPermissions();
const authzPerms = useAuthzManagementPermissions();
const { hasAtLeastOneViewPermissionsGrant, hasAttempted: hasAttemptedPermissions } = authzPerms;

const onTabClick = useCallback(
(key: string) => {
Expand All @@ -36,7 +37,7 @@ const AccessTabs = () => {
);

if (hasAttemptedPermissions && !hasAtLeastOneViewPermissionsGrant) {
return <ForbiddenContent message="You do not have permission to view grants and groups." />;
return <ForbiddenContent message="You do not have permission to view grants and groups." debugState={authzPerms} />;
}
return <Tabs type="card" activeKey={tab} onTabClick={onTabClick} items={TAB_ITEMS} />;
};
Expand Down
Loading

0 comments on commit 18a4f7c

Please sign in to comment.