added permission service to frontend to allow checking for permissions w/ burnettk
This commit is contained in:
parent
7444e0a62c
commit
b62955deaa
|
@ -51,6 +51,18 @@ permissions:
|
|||
allowed_permissions: [create, read, update, delete]
|
||||
uri: /v1.0/tasks/*
|
||||
|
||||
process-model-read-all:
|
||||
groups: [everybody]
|
||||
users: []
|
||||
allowed_permissions: [read]
|
||||
uri: /v1.0/process-models/*
|
||||
|
||||
process-group-read-all:
|
||||
groups: [everybody]
|
||||
users: []
|
||||
allowed_permissions: [read]
|
||||
uri: /v1.0/process-groups/*
|
||||
|
||||
# TODO: all uris should really have the same structure
|
||||
finance-admin-group:
|
||||
groups: ["Finance Team"]
|
||||
|
|
|
@ -11,6 +11,7 @@ from flask import request
|
|||
from flask_bpmn.api.api_error import ApiError
|
||||
from flask_bpmn.models.db import db
|
||||
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy import text
|
||||
|
||||
from spiffworkflow_backend.models.active_task import ActiveTaskModel
|
||||
|
@ -57,7 +58,14 @@ class AuthorizationService:
|
|||
)
|
||||
.filter_by(permission=permission)
|
||||
.join(PermissionTargetModel)
|
||||
.filter(text(f"'{target_uri}' LIKE permission_target.uri"))
|
||||
.filter(
|
||||
or_(
|
||||
text(f"'{target_uri}' LIKE permission_target.uri"),
|
||||
# to check for exact matches as well
|
||||
# see test_user_can_access_base_path_when_given_wildcard_permission unit test
|
||||
text(f"'{target_uri}' = replace(permission_target.uri, '/%', '')"),
|
||||
)
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
|
|
@ -163,3 +163,29 @@ class TestPermissions(BaseTest):
|
|||
self.assert_user_has_permission(
|
||||
group_a_admin, "update", f"/{process_group_b_id}"
|
||||
)
|
||||
|
||||
def test_user_can_access_base_path_when_given_wildcard_permission(
|
||||
self, app: Flask, with_db_and_bpmn_file_cleanup: None
|
||||
) -> None:
|
||||
"""Test_user_can_access_base_path_when_given_wildcard_permission."""
|
||||
group_a_admin = self.find_or_create_user()
|
||||
|
||||
permission_target = PermissionTargetModel(uri="/process-models/%")
|
||||
db.session.add(permission_target)
|
||||
db.session.commit()
|
||||
|
||||
permission_assignment = PermissionAssignmentModel(
|
||||
permission_target_id=permission_target.id,
|
||||
principal_id=group_a_admin.principal.id,
|
||||
permission="update",
|
||||
grant_type="permit",
|
||||
)
|
||||
db.session.add(permission_assignment)
|
||||
db.session.commit()
|
||||
|
||||
self.assert_user_has_permission(group_a_admin, "update", "/process-models/hey")
|
||||
self.assert_user_has_permission(group_a_admin, "update", "/process-models/")
|
||||
self.assert_user_has_permission(group_a_admin, "update", "/process-models")
|
||||
self.assert_user_has_permission(
|
||||
group_a_admin, "update", "/process-modelshey", expected_result=False
|
||||
)
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
"@carbon/icons-react": "^11.10.0",
|
||||
"@carbon/react": "^1.16.0",
|
||||
"@carbon/styles": "^1.16.0",
|
||||
"@casl/ability": "^6.3.2",
|
||||
"@casl/react": "^3.1.0",
|
||||
"@ginkgo-bioworks/react-json-schema-form-builder": "^2.9.0",
|
||||
"@monaco-editor/react": "^4.4.5",
|
||||
"@rjsf/core": "^4.2.0",
|
||||
|
@ -2276,6 +2278,26 @@
|
|||
"@carbon/layout": "^11.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@casl/ability": {
|
||||
"version": "6.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.3.2.tgz",
|
||||
"integrity": "sha512-ygOlg3WDu39t1ZOVdDfRpPXEiCn7F/a7uLBJIuAE6KksdBogzPszFRAuGULmo4h37fXIyouYUilVIryh0ddTRA==",
|
||||
"dependencies": {
|
||||
"@ucast/mongo2js": "^1.3.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/stalniy/casl/blob/master/BACKERS.md"
|
||||
}
|
||||
},
|
||||
"node_modules/@casl/react": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@casl/react/-/react-3.1.0.tgz",
|
||||
"integrity": "sha512-p4Xmex1Slxz/G0cBtZik+xyOkeOynBUe0UrMFTai6aYkYOb4NyUy3w+9rtnedjcuKijiow2HKJQjnSurLxdc/g==",
|
||||
"peerDependencies": {
|
||||
"@casl/ability": "^3.0.0 || ^4.0.0 || ^5.1.0 || ^6.0.0",
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.3.0.tgz",
|
||||
|
@ -5889,6 +5911,37 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@ucast/core": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.1.tgz",
|
||||
"integrity": "sha512-sXKbvQiagjFh2JCpaHUa64P4UdJbOxYeC5xiZFn8y6iYdb0WkismduE+RmiJrIjw/eLDYmIEXiQeIYYowmkcAw=="
|
||||
},
|
||||
"node_modules/@ucast/js": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.2.tgz",
|
||||
"integrity": "sha512-zxNkdIPVvqJjHI7D/iK8Aai1+59yqU+N7bpHFodVmiTN7ukeNiGGpNmmSjQgsUw7eNcEBnPrZHNzp5UBxwmaPw==",
|
||||
"dependencies": {
|
||||
"@ucast/core": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ucast/mongo": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.2.tgz",
|
||||
"integrity": "sha512-/zH1TdBJlYGKKD+Wh0oyD+aBvDSWrwHcD8b4tUL9UgHLhzHtkEnMVFuxbw3SRIRsAa01wmy06+LWt+WoZdj1Bw==",
|
||||
"dependencies": {
|
||||
"@ucast/core": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ucast/mongo2js": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.3.tgz",
|
||||
"integrity": "sha512-sBPtMUYg+hRnYeVYKL+ATm8FaRPdlU9PijMhGYKgsPGjV9J4Ks41ytIjGayvKUnBOEhiCaKUUnY4qPeifdqATw==",
|
||||
"dependencies": {
|
||||
"@ucast/core": "^1.6.1",
|
||||
"@ucast/js": "^3.0.0",
|
||||
"@ucast/mongo": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/copy-to-clipboard": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/copy-to-clipboard/-/copy-to-clipboard-1.0.12.tgz",
|
||||
|
@ -31852,6 +31905,20 @@
|
|||
"@carbon/layout": "^11.7.0"
|
||||
}
|
||||
},
|
||||
"@casl/ability": {
|
||||
"version": "6.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.3.2.tgz",
|
||||
"integrity": "sha512-ygOlg3WDu39t1ZOVdDfRpPXEiCn7F/a7uLBJIuAE6KksdBogzPszFRAuGULmo4h37fXIyouYUilVIryh0ddTRA==",
|
||||
"requires": {
|
||||
"@ucast/mongo2js": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"@casl/react": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@casl/react/-/react-3.1.0.tgz",
|
||||
"integrity": "sha512-p4Xmex1Slxz/G0cBtZik+xyOkeOynBUe0UrMFTai6aYkYOb4NyUy3w+9rtnedjcuKijiow2HKJQjnSurLxdc/g==",
|
||||
"requires": {}
|
||||
},
|
||||
"@codemirror/autocomplete": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.3.0.tgz",
|
||||
|
@ -34534,6 +34601,37 @@
|
|||
"eslint-visitor-keys": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"@ucast/core": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.1.tgz",
|
||||
"integrity": "sha512-sXKbvQiagjFh2JCpaHUa64P4UdJbOxYeC5xiZFn8y6iYdb0WkismduE+RmiJrIjw/eLDYmIEXiQeIYYowmkcAw=="
|
||||
},
|
||||
"@ucast/js": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.2.tgz",
|
||||
"integrity": "sha512-zxNkdIPVvqJjHI7D/iK8Aai1+59yqU+N7bpHFodVmiTN7ukeNiGGpNmmSjQgsUw7eNcEBnPrZHNzp5UBxwmaPw==",
|
||||
"requires": {
|
||||
"@ucast/core": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@ucast/mongo": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.2.tgz",
|
||||
"integrity": "sha512-/zH1TdBJlYGKKD+Wh0oyD+aBvDSWrwHcD8b4tUL9UgHLhzHtkEnMVFuxbw3SRIRsAa01wmy06+LWt+WoZdj1Bw==",
|
||||
"requires": {
|
||||
"@ucast/core": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"@ucast/mongo2js": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.3.tgz",
|
||||
"integrity": "sha512-sBPtMUYg+hRnYeVYKL+ATm8FaRPdlU9PijMhGYKgsPGjV9J4Ks41ytIjGayvKUnBOEhiCaKUUnY4qPeifdqATw==",
|
||||
"requires": {
|
||||
"@ucast/core": "^1.6.1",
|
||||
"@ucast/js": "^3.0.0",
|
||||
"@ucast/mongo": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@uiw/copy-to-clipboard": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/copy-to-clipboard/-/copy-to-clipboard-1.0.12.tgz",
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
"@carbon/icons-react": "^11.10.0",
|
||||
"@carbon/react": "^1.16.0",
|
||||
"@carbon/styles": "^1.16.0",
|
||||
"@casl/ability": "^6.3.2",
|
||||
"@casl/react": "^3.1.0",
|
||||
"@ginkgo-bioworks/react-json-schema-form-builder": "^2.9.0",
|
||||
"@monaco-editor/react": "^4.4.5",
|
||||
"@rjsf/core": "^4.2.0",
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useMemo, useState } from 'react';
|
|||
import { Content } from '@carbon/react';
|
||||
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { defineAbility } from '@casl/ability';
|
||||
import ErrorContext from './contexts/ErrorContext';
|
||||
import NavigationBar from './components/NavigationBar';
|
||||
|
||||
|
@ -11,6 +12,8 @@ import ErrorBoundary from './components/ErrorBoundary';
|
|||
import AdminRoutes from './routes/AdminRoutes';
|
||||
import { ErrorForDisplay } from './interfaces';
|
||||
|
||||
import { AbilityContext } from './contexts/Can';
|
||||
|
||||
export default function App() {
|
||||
const [errorMessage, setErrorMessage] = useState<ErrorForDisplay | null>(
|
||||
null
|
||||
|
@ -21,6 +24,8 @@ export default function App() {
|
|||
[errorMessage]
|
||||
);
|
||||
|
||||
const ability = defineAbility((can: any) => {});
|
||||
|
||||
let errorTag = null;
|
||||
if (errorMessage) {
|
||||
let sentryLinkTag = null;
|
||||
|
@ -46,21 +51,24 @@ export default function App() {
|
|||
|
||||
return (
|
||||
<div className="cds--white">
|
||||
<ErrorContext.Provider value={errorContextValueArray}>
|
||||
<BrowserRouter>
|
||||
<NavigationBar />
|
||||
<Content>
|
||||
{errorTag}
|
||||
<ErrorBoundary>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePageRoutes />} />
|
||||
<Route path="/tasks/*" element={<HomePageRoutes />} />
|
||||
<Route path="/admin/*" element={<AdminRoutes />} />
|
||||
</Routes>
|
||||
</ErrorBoundary>
|
||||
</Content>
|
||||
</BrowserRouter>
|
||||
</ErrorContext.Provider>
|
||||
{/* @ts-ignore */}
|
||||
<AbilityContext.Provider value={ability}>
|
||||
<ErrorContext.Provider value={errorContextValueArray}>
|
||||
<BrowserRouter>
|
||||
<NavigationBar />
|
||||
<Content>
|
||||
{errorTag}
|
||||
<ErrorBoundary>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePageRoutes />} />
|
||||
<Route path="/tasks/*" element={<HomePageRoutes />} />
|
||||
<Route path="/admin/*" element={<AdminRoutes />} />
|
||||
</Routes>
|
||||
</ErrorBoundary>
|
||||
</Content>
|
||||
</BrowserRouter>
|
||||
</ErrorContext.Provider>
|
||||
</AbilityContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { AbilityBuilder, Ability } from '@casl/ability';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { AbilityContext } from '../contexts/Can';
|
||||
import { PermissionCheckResponseBody, PermissionsToCheck } from '../interfaces';
|
||||
import HttpService from '../services/HttpService';
|
||||
|
||||
export const usePermissionFetcher = (
|
||||
permissionsToCheck: PermissionsToCheck
|
||||
) => {
|
||||
const ability = useContext(AbilityContext);
|
||||
|
||||
useEffect(() => {
|
||||
const processPermissionResult = (result: PermissionCheckResponseBody) => {
|
||||
const { can, cannot, rules } = new AbilityBuilder(Ability);
|
||||
for (const [url, permissionVerbResults] of Object.entries(
|
||||
result.results
|
||||
)) {
|
||||
for (const [permissionVerb, hasPermission] of Object.entries(
|
||||
permissionVerbResults
|
||||
)) {
|
||||
if (hasPermission) {
|
||||
can(permissionVerb, url);
|
||||
} else {
|
||||
cannot(permissionVerb, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
ability.update(rules);
|
||||
};
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/permissions-check`,
|
||||
httpMethod: 'POST',
|
||||
successCallback: processPermissionResult,
|
||||
postBody: { requests_to_check: permissionsToCheck },
|
||||
// failureCallback: setErrorMessage,
|
||||
});
|
||||
});
|
||||
|
||||
return { ability };
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
import { createContext } from 'react';
|
||||
import { AbilityBuilder, Ability } from '@casl/ability';
|
||||
import { createContextualCan } from '@casl/react';
|
||||
|
||||
export const AbilityContext = createContext(new Ability());
|
||||
export const Can = createContextualCan(AbilityContext.Consumer);
|
|
@ -70,3 +70,16 @@ export interface PaginationObject {
|
|||
export interface CarbonComboBoxSelection {
|
||||
selectedItem: ProcessModel;
|
||||
}
|
||||
|
||||
export interface PermissionsToCheck {
|
||||
[key: string]: string[];
|
||||
}
|
||||
export interface PermissionCheckResponseBody {
|
||||
results: PermissionCheckResult;
|
||||
}
|
||||
export interface PermissionCheckResult {
|
||||
[key: string]: PermissionVerbResults;
|
||||
}
|
||||
export interface PermissionVerbResults {
|
||||
[key: string]: boolean;
|
||||
}
|
||||
|
|
|
@ -107,7 +107,7 @@ export default function AdminRoutes() {
|
|||
/>
|
||||
<Route path="process-instances" element={<ProcessInstanceList />} />
|
||||
<Route path="messages" element={<MessageInstanceList />} />
|
||||
<Route path="/configuration/*" element={<Configuration />} />
|
||||
<Route path="configuration/*" element={<Configuration />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -74,7 +74,6 @@ export default function ProcessInstanceLogList() {
|
|||
};
|
||||
|
||||
if (pagination) {
|
||||
console.log('params.process_model_id', params.process_model_id);
|
||||
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
|
||||
return (
|
||||
<main>
|
||||
|
|
|
@ -27,13 +27,20 @@ import {
|
|||
TableBody,
|
||||
// @ts-ignore
|
||||
} from '@carbon/react';
|
||||
import { Can } from '@casl/react';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import HttpService from '../services/HttpService';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import { modifyProcessModelPath } from '../helpers';
|
||||
import { ProcessFile, ProcessModel, RecentProcessModel } from '../interfaces';
|
||||
import {
|
||||
PermissionsToCheck,
|
||||
ProcessFile,
|
||||
ProcessModel,
|
||||
RecentProcessModel,
|
||||
} from '../interfaces';
|
||||
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
|
||||
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
|
||||
import { usePermissionFetcher } from '../components/PermissionService';
|
||||
|
||||
const storeRecentProcessModelInLocalStorage = (
|
||||
processModelForStorage: ProcessModel
|
||||
|
@ -96,6 +103,19 @@ export default function ProcessModelShow() {
|
|||
useState<boolean>(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const targetUris = {
|
||||
processModelPath: `/process-models/${params.process_model_id}`,
|
||||
processInstancesPath: `/process-instances`,
|
||||
};
|
||||
const permissionRequestData: PermissionsToCheck = {
|
||||
[`/v1.0${targetUris.processModelPath}`]: ['GET', 'PUT'],
|
||||
[`/v1.0${targetUris.processInstancesPath}`]: ['GET'],
|
||||
[`/v1.0${targetUris.processModelPath}${targetUris.processInstancesPath}`]: [
|
||||
'POST',
|
||||
],
|
||||
};
|
||||
const { ability } = usePermissionFetcher(permissionRequestData);
|
||||
|
||||
const modifiedProcessModelId = modifyProcessModelPath(
|
||||
`${params.process_model_id}`
|
||||
);
|
||||
|
@ -130,7 +150,7 @@ export default function ProcessModelShow() {
|
|||
});
|
||||
};
|
||||
|
||||
const processInstanceResultTag = () => {
|
||||
const processInstanceRunResultTag = () => {
|
||||
if (processModel && processInstanceResult) {
|
||||
// FIXME: ensure that the task is actually for the current user as well
|
||||
const processInstanceId = (processInstanceResult as any).id;
|
||||
|
@ -491,26 +511,44 @@ export default function ProcessModelShow() {
|
|||
<h1>Process Model: {processModel.display_name}</h1>
|
||||
<p className="process-description">{processModel.description}</p>
|
||||
<Stack orientation="horizontal" gap={3}>
|
||||
<Button onClick={processInstanceCreateAndRun} variant="primary">
|
||||
Run
|
||||
</Button>
|
||||
<Button
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/edit`}
|
||||
variant="secondary"
|
||||
<Can
|
||||
I="POST"
|
||||
a={`/v1.0${targetUris.processModelPath}${targetUris.processInstancesPath}`}
|
||||
ability={ability}
|
||||
>
|
||||
Edit process model
|
||||
</Button>
|
||||
<Button onClick={processInstanceCreateAndRun} variant="primary">
|
||||
Run
|
||||
</Button>
|
||||
</Can>
|
||||
<Can
|
||||
I="PUT"
|
||||
a={`/v1.0${targetUris.processModelPath}`}
|
||||
ability={ability}
|
||||
>
|
||||
<Button
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/edit`}
|
||||
variant="secondary"
|
||||
>
|
||||
Edit process model
|
||||
</Button>
|
||||
</Can>
|
||||
</Stack>
|
||||
<br />
|
||||
<br />
|
||||
{processInstanceResultTag()}
|
||||
<br />
|
||||
<ProcessInstanceListTable
|
||||
filtersEnabled={false}
|
||||
processModelFullIdentifier={processModel.id}
|
||||
perPageOptions={[2, 5, 25]}
|
||||
/>
|
||||
{processInstanceRunResultTag()}
|
||||
<br />
|
||||
<Can
|
||||
I="GET"
|
||||
a={`/v1.0${targetUris.processInstancesPath}`}
|
||||
ability={ability}
|
||||
>
|
||||
<ProcessInstanceListTable
|
||||
filtersEnabled={false}
|
||||
processModelFullIdentifier={processModel.id}
|
||||
perPageOptions={[2, 5, 25]}
|
||||
/>
|
||||
<br />
|
||||
</Can>
|
||||
{processModelButtons()}
|
||||
</>
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue