filter process models based on user permissions on the backend if specified w/ burnettk

This commit is contained in:
jasquat 2022-11-22 16:21:16 -05:00
parent ea45b046b9
commit d2a5faaed3
13 changed files with 186 additions and 91 deletions

View File

@ -278,7 +278,13 @@ paths:
required: false required: false
description: Get all sub process models recursively if true description: Get all sub process models recursively if true
schema: schema:
type: string type: boolean
- name: filter_runnable_by_user
in: query
required: false
description: Get only the process models that the user can run
schema:
type: boolean
- name: page - name: page
in: query in: query
required: false required: false

View File

@ -53,8 +53,8 @@ groups:
lead, lead,
] ]
hr: core-contributor:
users: [manuchehr] users: [core]
permissions: permissions:
tasks-crud: tasks-crud:
@ -62,26 +62,6 @@ permissions:
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/* 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/*
process-instance-list:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-instances
process-instance-report-list:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-instances/reports
admin: admin:
groups: [admin] groups: [admin]
@ -90,7 +70,7 @@ permissions:
uri: /* uri: /*
read-all: read-all:
groups: ["Finance Team", "Project Lead", hr, admin] groups: ["Finance Team", "Project Lead", admin]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /* uri: /*
@ -156,3 +136,39 @@ permissions:
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement:procurement:vendor-invoice-management/* uri: /v1.0/process-instances/manage-procurement:procurement:vendor-invoice-management/*
core-admin:
groups: ["core-contributor"]
users: []
allowed_permissions: [read]
uri: /v1.0/process-groups/manage-procurement:procurement:vendor-invoice-management:*
core-admin-slash:
groups: ["core-contributor"]
users: []
allowed_permissions: [read]
uri: /v1.0/process-groups/manage-procurement:procurement:vendor-invoice-management/*
core-admin-models:
groups: ["core-contributor"]
users: []
allowed_permissions: [read]
uri: /v1.0/process-models/manage-procurement:procurement:vendor-invoice-management:*
core-admin-models-slash:
groups: ["core-contributor"]
users: []
allowed_permissions: [read]
uri: /v1.0/process-models/manage-procurement:procurement:vendor-invoice-management/*
core-admin-models-instantiate:
groups: ["core-contributor"]
users: []
allowed_permissions: [create]
uri: /v1.0/process-models/manage-procurement:procurement:vendor-invoice-management:invoice-approval/process-instances
core-admin-instances:
groups: ["core-contributor"]
users: []
allowed_permissions: [create]
uri: /v1.0/process-instances/manage-procurement:procurement:vendor-invoice-management:*
core-admin-instances-slash:
groups: ["core-contributor"]
users: []
allowed_permissions: [create]
uri: /v1.0/process-instances/manage-procurement:procurement:vendor-invoice-management/*

View File

@ -53,8 +53,8 @@ groups:
lead, lead,
] ]
hr: core-contributor:
users: [manuchehr] users: [core]
permissions: permissions:
tasks-crud: tasks-crud:
@ -70,7 +70,7 @@ permissions:
uri: /* uri: /*
read-all: read-all:
groups: ["Finance Team", "Project Lead", hr, admin] groups: ["Finance Team", "Project Lead", admin]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /* uri: /*
@ -136,3 +136,39 @@ permissions:
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement:procurement:vendor-invoice-management/* uri: /v1.0/process-instances/manage-procurement:procurement:vendor-invoice-management/*
core-admin:
groups: ["core-contributor"]
users: []
allowed_permissions: [read]
uri: /v1.0/process-groups/manage-procurement:procurement:vendor-invoice-management:*
core-admin-slash:
groups: ["core-contributor"]
users: []
allowed_permissions: [read]
uri: /v1.0/process-groups/manage-procurement:procurement:vendor-invoice-management/*
core-admin-models:
groups: ["core-contributor"]
users: []
allowed_permissions: [read]
uri: /v1.0/process-models/manage-procurement:procurement:vendor-invoice-management:*
core-admin-models-slash:
groups: ["core-contributor"]
users: []
allowed_permissions: [read]
uri: /v1.0/process-models/manage-procurement:procurement:vendor-invoice-management/*
core-admin-models-instantiate:
groups: ["core-contributor"]
users: []
allowed_permissions: [create]
uri: /v1.0/process-models/manage-procurement:procurement:vendor-invoice-management:invoice-approval/process-instances
core-admin-instances:
groups: ["core-contributor"]
users: []
allowed_permissions: [create]
uri: /v1.0/process-instances/manage-procurement:procurement:vendor-invoice-management:*
core-admin-instances-slash:
groups: ["core-contributor"]
users: []
allowed_permissions: [create]
uri: /v1.0/process-instances/manage-procurement:procurement:vendor-invoice-management/*

View File

@ -349,12 +349,15 @@ def process_model_move(
def process_model_list( def process_model_list(
process_group_identifier: Optional[str] = None, process_group_identifier: Optional[str] = None,
recursive: Optional[bool] = False, recursive: Optional[bool] = False,
filter_runnable_by_user: Optional[bool] = False,
page: int = 1, page: int = 1,
per_page: int = 100, per_page: int = 100,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process model list!""" """Process model list!"""
process_models = ProcessModelService().get_process_models( process_models = ProcessModelService().get_process_models(
process_group_id=process_group_identifier, recursive=recursive process_group_id=process_group_identifier,
recursive=recursive,
filter_runnable_by_user=filter_runnable_by_user,
) )
batch = ProcessModelService().get_batch( batch = ProcessModelService().get_batch(
process_models, page=page, per_page=per_page process_models, page=page, per_page=per_page
@ -1319,7 +1322,7 @@ def task_submit(
task_id, process_instance, processor=processor task_id, process_instance, processor=processor
) )
AuthorizationService.assert_user_can_complete_spiff_task( AuthorizationService.assert_user_can_complete_spiff_task(
processor, spiff_task, principal.user process_instance.id, spiff_task, principal.user
) )
if spiff_task.state != TaskState.READY: if spiff_task.state != TaskState.READY:

View File

@ -24,9 +24,6 @@ from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.user import UserNotFoundError from spiffworkflow_backend.models.user import UserNotFoundError
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel
from spiffworkflow_backend.services.group_service import GroupService from spiffworkflow_backend.services.group_service import GroupService
from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor,
)
from spiffworkflow_backend.services.user_service import UserService from spiffworkflow_backend.services.user_service import UserService
@ -393,25 +390,25 @@ class AuthorizationService:
@staticmethod @staticmethod
def assert_user_can_complete_spiff_task( def assert_user_can_complete_spiff_task(
processor: ProcessInstanceProcessor, process_instance_id: int,
spiff_task: SpiffTask, spiff_task: SpiffTask,
user: UserModel, user: UserModel,
) -> bool: ) -> bool:
"""Assert_user_can_complete_spiff_task.""" """Assert_user_can_complete_spiff_task."""
active_task = ActiveTaskModel.query.filter_by( active_task = ActiveTaskModel.query.filter_by(
task_name=spiff_task.task_spec.name, task_name=spiff_task.task_spec.name,
process_instance_id=processor.process_instance_model.id, process_instance_id=process_instance_id,
).first() ).first()
if active_task is None: if active_task is None:
raise ActiveTaskNotFoundError( raise ActiveTaskNotFoundError(
f"Could find an active task with task name '{spiff_task.task_spec.name}'" f"Could find an active task with task name '{spiff_task.task_spec.name}'"
f" for process instance '{processor.process_instance_model.id}'" f" for process instance '{process_instance_id}'"
) )
if user not in active_task.potential_owners: if user not in active_task.potential_owners:
raise UserDoesNotHaveAccessToTaskError( raise UserDoesNotHaveAccessToTaskError(
f"User {user.username} does not have access to update task'{spiff_task.task_spec.name}'" f"User {user.username} does not have access to update task'{spiff_task.task_spec.name}'"
f" for process instance '{processor.process_instance_model.id}'" f" for process instance '{process_instance_id}'"
) )
return True return True

View File

@ -197,7 +197,7 @@ class ProcessInstanceService:
a multi-instance task. a multi-instance task.
""" """
AuthorizationService.assert_user_can_complete_spiff_task( AuthorizationService.assert_user_can_complete_spiff_task(
processor, spiff_task, user processor.process_instance_model.id, spiff_task, user
) )
dot_dct = ProcessInstanceService.create_dot_dict(data) dot_dct = ProcessInstanceService.create_dot_dict(data)

View File

@ -18,7 +18,9 @@ from spiffworkflow_backend.models.process_group import ProcessGroupSchema
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema
from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.file_system_service import FileSystemService from spiffworkflow_backend.services.file_system_service import FileSystemService
from spiffworkflow_backend.services.user_service import UserService
T = TypeVar("T") T = TypeVar("T")
@ -179,7 +181,10 @@ class ProcessModelService(FileSystemService):
raise ProcessEntityNotFoundError("process_model_not_found") raise ProcessEntityNotFoundError("process_model_not_found")
def get_process_models( def get_process_models(
self, process_group_id: Optional[str] = None, recursive: Optional[bool] = False self,
process_group_id: Optional[str] = None,
recursive: Optional[bool] = False,
filter_runnable_by_user: Optional[bool] = False,
) -> List[ProcessModelInfo]: ) -> List[ProcessModelInfo]:
"""Get process models.""" """Get process models."""
process_models = [] process_models = []
@ -201,6 +206,19 @@ class ProcessModelService(FileSystemService):
) )
process_models.append(process_model) process_models.append(process_model)
process_models.sort() process_models.sort()
if filter_runnable_by_user:
user = UserService.current_user()
new_process_model_list = []
for process_model in process_models:
uri = f"/v1.0/process-models/{process_model.id.replace('/', ':')}/process-instances"
result = AuthorizationService.user_has_permission(
user=user, permission="create", target_uri=uri
)
if result:
new_process_model_list.append(process_model)
return new_process_model_list
return process_models return process_models
def get_process_groups( def get_process_groups(

View File

@ -4,10 +4,16 @@ import {
Button, Button,
// @ts-ignore // @ts-ignore
} from '@carbon/react'; } from '@carbon/react';
import { ProcessModel, RecentProcessModel } from '../interfaces'; import { Can } from '@casl/react';
import {
PermissionsToCheck,
ProcessModel,
RecentProcessModel,
} from '../interfaces';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import ErrorContext from '../contexts/ErrorContext'; import ErrorContext from '../contexts/ErrorContext';
import { modifyProcessIdentifierForPathParam } from '../helpers'; import { modifyProcessIdentifierForPathParam } from '../helpers';
import { usePermissionFetcher } from '../hooks/PermissionService';
const storeRecentProcessModelInLocalStorage = ( const storeRecentProcessModelInLocalStorage = (
processModelForStorage: ProcessModel processModelForStorage: ProcessModel
@ -75,6 +81,12 @@ export default function ProcessInstanceRun({
processModel.id processModel.id
); );
const processInstanceActionPath = `/v1.0/process-models/${modifiedProcessModelId}/process-instances`;
const permissionRequestData: PermissionsToCheck = {
[processInstanceActionPath]: ['POST'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
const onProcessInstanceRun = (processInstance: any) => { const onProcessInstanceRun = (processInstance: any) => {
// FIXME: ensure that the task is actually for the current user as well // FIXME: ensure that the task is actually for the current user as well
const processInstanceId = (processInstance as any).id; const processInstanceId = (processInstance as any).id;
@ -98,15 +110,17 @@ export default function ProcessInstanceRun({
const processInstanceCreateAndRun = () => { const processInstanceCreateAndRun = () => {
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}/process-instances`, path: processInstanceActionPath,
successCallback: processModelRun, successCallback: processModelRun,
httpMethod: 'POST', httpMethod: 'POST',
}); });
}; };
return ( return (
<Button onClick={processInstanceCreateAndRun} className={className}> <Can I="POST" a={processInstanceActionPath} ability={ability}>
Run <Button onClick={processInstanceCreateAndRun} className={className}>
</Button> Run
</Button>
</Can>
); );
} }

View File

@ -33,11 +33,11 @@ export default function ProcessModelListTiles({
setProcessModels(result.results); setProcessModels(result.results);
}; };
// only allow 10 for now until we get the backend only returning certain models for user execution // only allow 10 for now until we get the backend only returning certain models for user execution
let queryParams = '?per_page=1000'; let queryParams = '?per_page=20';
if (processGroup) { if (processGroup) {
queryParams = `${queryParams}&process_group_identifier=${processGroup.id}`; queryParams = `${queryParams}&process_group_identifier=${processGroup.id}`;
} else { } else {
queryParams = `${queryParams}&recursive=true`; queryParams = `${queryParams}&recursive=true&filter_runnable_by_user=true`;
} }
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/process-models${queryParams}`, path: `/process-models${queryParams}`,

View File

@ -256,3 +256,7 @@ in on this with the react-jsonschema-form repo. This is just a patch fix to allo
.clear-left { .clear-left {
clear: left; clear: left;
} }
td.actions-cell {
width: 1em;
}

View File

@ -18,7 +18,7 @@ import {
import ProcessInstanceRun from '../components/ProcessInstanceRun'; import ProcessInstanceRun from '../components/ProcessInstanceRun';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5; const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const REFRESH_INTERVAL = 10; const REFRESH_INTERVAL = 5;
const REFRESH_TIMEOUT = 600; const REFRESH_TIMEOUT = 600;
export default function MyTasks() { export default function MyTasks() {
@ -158,7 +158,7 @@ export default function MyTasks() {
{row.processModelDisplayName} {row.processModelDisplayName}
</Link> </Link>
</td> </td>
<td> <td className="actions-cell">
<ProcessInstanceRun <ProcessInstanceRun
processModel={processModel} processModel={processModel}
onSuccessCallback={setProcessInstance} onSuccessCallback={setProcessInstance}

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
Link, // Link,
useSearchParams, useSearchParams,
useParams, useParams,
useNavigate, useNavigate,
@ -11,10 +11,9 @@ import {
// @ts-ignore // @ts-ignore
} from '@carbon/icons-react'; } from '@carbon/icons-react';
// @ts-ignore // @ts-ignore
import { Button, Table, Stack } from '@carbon/react'; import { Button, Stack } from '@carbon/react';
import { Can } from '@casl/react'; import { Can } from '@casl/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import PaginationForTable from '../components/PaginationForTable';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import { import {
getPageInfoFromSearchParams, getPageInfoFromSearchParams,
@ -25,7 +24,7 @@ import {
PaginationObject, PaginationObject,
PermissionsToCheck, PermissionsToCheck,
ProcessGroup, ProcessGroup,
ProcessModel, // ProcessModel,
} from '../interfaces'; } from '../interfaces';
import { useUriListForPermissions } from '../hooks/UriListForPermissions'; import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { usePermissionFetcher } from '../hooks/PermissionService'; import { usePermissionFetcher } from '../hooks/PermissionService';
@ -39,7 +38,7 @@ export default function ProcessGroupShow() {
const navigate = useNavigate(); const navigate = useNavigate();
const [processGroup, setProcessGroup] = useState<ProcessGroup | null>(null); const [processGroup, setProcessGroup] = useState<ProcessGroup | null>(null);
const [processModels, setProcessModels] = useState([]); // const [processModels, setProcessModels] = useState([]);
const [modelPagination, setModelPagination] = const [modelPagination, setModelPagination] =
useState<PaginationObject | null>(null); useState<PaginationObject | null>(null);
@ -55,7 +54,7 @@ export default function ProcessGroupShow() {
const { page, perPage } = getPageInfoFromSearchParams(searchParams); const { page, perPage } = getPageInfoFromSearchParams(searchParams);
const setProcessModelFromResult = (result: any) => { const setProcessModelFromResult = (result: any) => {
setProcessModels(result.results); // setProcessModels(result.results);
setModelPagination(result.pagination); setModelPagination(result.pagination);
}; };
const processResult = (result: any) => { const processResult = (result: any) => {
@ -74,42 +73,42 @@ export default function ProcessGroupShow() {
}); });
}, [params, searchParams]); }, [params, searchParams]);
const buildModelTable = () => { // const buildModelTable = () => {
if (processGroup === null) { // if (processGroup === null) {
return null; // return null;
} // }
const rows = processModels.map((row: ProcessModel) => { // const rows = processModels.map((row: ProcessModel) => {
const modifiedProcessModelId: String = // const modifiedProcessModelId: String =
modifyProcessIdentifierForPathParam((row as any).id); // modifyProcessIdentifierForPathParam((row as any).id);
return ( // return (
<tr key={row.id}> // <tr key={row.id}>
<td> // <td>
<Link // <Link
to={`/admin/process-models/${modifiedProcessModelId}`} // to={`/admin/process-models/${modifiedProcessModelId}`}
data-qa="process-model-show-link" // data-qa="process-model-show-link"
> // >
{row.id} // {row.id}
</Link> // </Link>
</td> // </td>
<td>{row.display_name}</td> // <td>{row.display_name}</td>
</tr> // </tr>
); // );
}); // });
return ( // return (
<div> // <div>
<h2>Process Models</h2> // <h2>Process Models</h2>
<Table striped bordered> // <Table striped bordered>
<thead> // <thead>
<tr> // <tr>
<th>Process Model Id</th> // <th>Process Model Id</th>
<th>Display Name</th> // <th>Display Name</th>
</tr> // </tr>
</thead> // </thead>
<tbody>{rows}</tbody> // <tbody>{rows}</tbody>
</Table> // </Table>
</div> // </div>
); // );
}; // };
const navigateToProcessGroups = (_result: any) => { const navigateToProcessGroups = (_result: any) => {
navigate(`/admin/process-groups`); navigate(`/admin/process-groups`);
@ -128,7 +127,7 @@ export default function ProcessGroupShow() {
}; };
if (processGroup && modelPagination) { if (processGroup && modelPagination) {
const { page, perPage } = getPageInfoFromSearchParams(searchParams); // const { page, perPage } = getPageInfoFromSearchParams(searchParams);
const modifiedProcessGroupId = modifyProcessIdentifierForPathParam( const modifiedProcessGroupId = modifyProcessIdentifierForPathParam(
processGroup.id processGroup.id
); );

View File

@ -66,9 +66,11 @@ backendCallProps) => {
method: httpMethod, method: httpMethod,
}); });
const updatedPath = path.replace(/^\/v1\.0/, '');
let isSuccessful = true; let isSuccessful = true;
let is403 = false; let is403 = false;
fetch(`${BACKEND_BASE_URL}${path}`, httpArgs) fetch(`${BACKEND_BASE_URL}${updatedPath}`, httpArgs)
.then((response) => { .then((response) => {
if (response.status === 401) { if (response.status === 401) {
UserService.doLogin(); UserService.doLogin();