Skip to content

Commit 3c42411

Browse files
eenblamBen ElamMark90
authored
Add role based access control to running workflows (#931) (#939)
* Add utils/auth.py * Implement RBAC for resume and retry workflows Unlike new_process, this can be checked immediately in the request handler. Policy priorities specified via workflow and steps are resolved via get_auth_callbacks. * Implement RBAC for processes started via celery * Fix bug and linting issues * Refactor get_auth_callbacks; add tests * Use functional approach for get_auth_callbacks * Parametrize new tests * Add docs for workflow authorization * Improve process filtering on resume endpoint * Add xfailed test for resume_process_endpoint * Change RBAC bold text to a warning banner * Bump version to 4.1.0rc2 --------- Co-authored-by: Ben Elam <baelam@es.net> Co-authored-by: Mark90 <mark_moes@live.nl>
1 parent 718cd18 commit 3c42411

File tree

10 files changed

+614
-37
lines changed

10 files changed

+614
-37
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 4.1.0rc1
2+
current_version = 4.1.0rc2
33
commit = False
44
tag = False
55
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(rc(?P<build>\d+))?

docs/reference-docs/auth-backend-and-frontend.md

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,323 @@ app.register_authorization(authorization_instance)
269269
app.register_graphql_authorization(graphql_authorization_instance)
270270
```
271271

272+
## Authorization and Workflows
273+
274+
!!! Warning
275+
Role-based access control for workflows is currently in beta.
276+
Initial support has been added to the backend, but the feature is not fully communicated through the UI yet.
277+
278+
Certain `orchestrator-core` decorators accept authorization callbacks of type `type Authorizer = Callable[OIDCUserModel, bool]`, which return True when the input user is authorized, otherwise False.
279+
280+
A table (below) is available for comparing possible configuration states with the policy that will be enforced.
281+
282+
### `@workflow`
283+
The `@workflow` decorator accepts the optional parameters `auth: Authorizer` and `retry_auth: Authorizer`.
284+
285+
`auth` will be used to determine the authorization of a user to start the workflow.
286+
If `auth` is omitted, the workflow is authorized for any logged in user.
287+
288+
`retry_auth` will be used to determine the authorization of a user to start, resume, or retry the workflow from a failed step.
289+
If `retry_auth` is omitted, then `auth` is used to authorize.
290+
291+
(This does not percolate past an `@inputstep` that specifies `resume_auth` or `retry_auth`.)
292+
293+
Examples:
294+
295+
* `auth=None, retry_auth=None`: any user may run the workflow.
296+
* `auth=A, retry_auth=B`: users authorized by A may start the workflow. Users authorized by B may retry on failure.
297+
* Example: starting the workflow is a decision that must be made by a product owner. Retrying can be made by an on-call member of the operations team.
298+
* `auth=None, retry_auth=B`: any user can start the workflow, but only users authorized by B may retry on failure.
299+
300+
### `@inputstep`
301+
The `@inputstep` decorator accepts the optional parameters `resume_auth: Authorizer` and `retry_auth: Authorizer`.
302+
303+
`resume_auth` will be used to determine the authorization of a user to resume the workflow when suspended at this inputstep.
304+
If `resume_auth` is omitted, then the workflow's `auth` will be used.
305+
306+
`retry_auth` will be used to determine the authorization of a user to retry the workflow from a failed step following the inputstep.
307+
If `retry_auth` is omitted, then `resume_auth` is used to authorize retries.
308+
If `resume_auth` is also omitted, then the workflows `retry_auth` is checked, and then the workflows `auth`.
309+
310+
In summary:
311+
312+
* A workflow establishes `auth` for starting, resuming, or retrying.
313+
* The workflow can also establish `retry_auth`, which will override `auth` for retries.
314+
* An inputstep can override the existing `auth` with `resume_auth` and the existing `retry_auth` with its own `retry_auth`.
315+
* Subsequent inputsteps can do the same, but any None will not overwrite a previous not-None.
316+
317+
### Policy resolutions
318+
Below is an exhaustive table of how policies (implemented as callbacks `A`, `B`, `C`, and `D`)
319+
are prioritized in different workflow and inputstep configurations.
320+
321+
<table>
322+
<thead>
323+
<tr>
324+
<th colspan=4>Configuration</th>
325+
<th colspan=4>Enforcement</th>
326+
<th>Notes</th>
327+
</tr>
328+
<tr>
329+
<th colspan=2>@workflow</th>
330+
<th colspan=2>@inputstep</th>
331+
<th colspan=2>before @inputstep</th>
332+
<th colspan=2>@inputstep and after</th>
333+
<th></th>
334+
</tr>
335+
<tr>
336+
<th>auth</th>
337+
<th>retry_auth</th>
338+
<th>resume_auth</th>
339+
<th>retry_auth</th>
340+
<th>start</th>
341+
<th>retry</th>
342+
<th>resume</th>
343+
<th>retry</th>
344+
<th></th>
345+
</tr>
346+
</thead>
347+
<tbody>
348+
<tr>
349+
<td>None</td>
350+
<td>None</td>
351+
<td>None</td>
352+
<td>None</td>
353+
<td>Anyone</td>
354+
<td>Anyone</td>
355+
<td>Anyone</td>
356+
<td>Anyone</td>
357+
<td>Default</td>
358+
</tr>
359+
<tr>
360+
<td>A</td>
361+
<td>None</td>
362+
<td>None</td>
363+
<td>None</td>
364+
<td>A</td>
365+
<td>A</td>
366+
<td>A</td>
367+
<td>A</td>
368+
<td>Broadly restrict the workflow to a specific authorizer.</td>
369+
</tr>
370+
<tr>
371+
<td>None</td>
372+
<td>B</td>
373+
<td>None</td>
374+
<td>None</td>
375+
<td>Anyone</td>
376+
<td>B</td>
377+
<td>Anyone</td>
378+
<td>B</td>
379+
<td>original retry_auth is maintained if nothing supercedes it. Weird choice, but this provides a "we specifically want to limit retries" route.</td>
380+
</tr>
381+
<tr>
382+
<td>A</td>
383+
<td>B</td>
384+
<td>None</td>
385+
<td>None</td>
386+
<td>A</td>
387+
<td>B</td>
388+
<td>A</td>
389+
<td>B</td>
390+
<td>Workflow-level auth and retry. Allows A or B to be tighter or distinct, as needed.</td>
391+
</tr>
392+
<tr>
393+
<td>None</td>
394+
<td>None</td>
395+
<td>C</td>
396+
<td>None</td>
397+
<td>Anyone</td>
398+
<td>Anyone</td>
399+
<td>C</td>
400+
<td>C</td>
401+
<td>Anyone can start this workflow, but only C can continue it.</td>
402+
</tr>
403+
<tr>
404+
<td>A</td>
405+
<td>None</td>
406+
<td>C</td>
407+
<td>None</td>
408+
<td>A</td>
409+
<td>A</td>
410+
<td>C</td>
411+
<td>C</td>
412+
<td>Subsequent retries use C, not A! Override with retry_auth=A if desired.</td>
413+
</tr>
414+
<tr>
415+
<td>None</td>
416+
<td>B</td>
417+
<td>C</td>
418+
<td>None</td>
419+
<td>Anyone</td>
420+
<td>B</td>
421+
<td>C</td>
422+
<td>C</td>
423+
<td>Subsequent retries use C, not B! Override with retry_auth=B if desired.</td>
424+
</tr>
425+
<tr>
426+
<td>A</td>
427+
<td>B</td>
428+
<td>C</td>
429+
<td>None</td>
430+
<td>A</td>
431+
<td>B</td>
432+
<td>C</td>
433+
<td>C</td>
434+
<td>Simple override initial settings with inputstep resume_auth.</td>
435+
</tr>
436+
<tr>
437+
<td>None</td>
438+
<td>None</td>
439+
<td>None</td>
440+
<td>D</td>
441+
<td>Anyone</td>
442+
<td>Anyone</td>
443+
<td>Anyone</td>
444+
<td>D</td>
445+
<td>Anyone can start or retry or resume, but limit retries to D once inputstep is reached.</td>
446+
</tr>
447+
<tr>
448+
<td>A</td>
449+
<td>None</td>
450+
<td>None</td>
451+
<td>D</td>
452+
<td>A</td>
453+
<td>A</td>
454+
<td>A</td>
455+
<td>D</td>
456+
<td>A can start or retry or resume, but limit retries to D once inputstep is reached.</td>
457+
</tr>
458+
<tr>
459+
<td>None</td>
460+
<td>B</td>
461+
<td>None</td>
462+
<td>D</td>
463+
<td>Anyone</td>
464+
<td>B</td>
465+
<td>Anyone</td>
466+
<td>D</td>
467+
<td>Anyone can start or resume, but only B can retry. After inputstep, only D can retry.</td>
468+
</tr>
469+
<tr>
470+
<td>A</td>
471+
<td>B</td>
472+
<td>None</td>
473+
<td>D</td>
474+
<td>A</td>
475+
<td>B</td>
476+
<td>A</td>
477+
<td>D</td>
478+
<td>A can start or resume, but only B can retry. After inputstep, only D can retry.</td>
479+
</tr>
480+
<tr>
481+
<td>None</td>
482+
<td>None</td>
483+
<td>C</td>
484+
<td>D</td>
485+
<td>Anyone</td>
486+
<td>Anyone</td>
487+
<td>C</td>
488+
<td>D</td>
489+
<td>Anyone can start, but only C can resume and only D can retry after the resume.</td>
490+
</tr>
491+
<tr>
492+
<td>A</td>
493+
<td>None</td>
494+
<td>C</td>
495+
<td>D</td>
496+
<td>A</td>
497+
<td>A</td>
498+
<td>C</td>
499+
<td>D</td>
500+
<td></td>
501+
</tr>
502+
<tr>
503+
<td>None</td>
504+
<td>B</td>
505+
<td>C</td>
506+
<td>D</td>
507+
<td>Anyone</td>
508+
<td>B</td>
509+
<td>C</td>
510+
<td>D</td>
511+
<td></td>
512+
</tr>
513+
<tr>
514+
<td>A</td>
515+
<td>B</td>
516+
<td>C</td>
517+
<td>D</td>
518+
<td>A</td>
519+
<td>B</td>
520+
<td>C</td>
521+
<td>D</td>
522+
<td></td>
523+
</tr>
524+
</tbody>
525+
</table>
526+
527+
### Examples
528+
Assume we have the following function that can be used to create callbacks:
529+
530+
```python
531+
def allow_roles(*roles) -> Callable[OIDCUserModel|None, bool]:
532+
def f(user: OIDCUserModel) -> bool:
533+
if is_admin(user): # Relative to your authorization provider
534+
return True
535+
for role in roles:
536+
if has_role(user, role): # Relative to your authorization provider
537+
return True
538+
return False
539+
540+
return f
541+
```
542+
543+
We can now construct a variety of authorization policies.
544+
545+
#### Rubber Stamp Model
546+
!!!example
547+
Suppose we have a workflow W that needs to pause on inputstep `approval` for approval from finance. Ops (and only ops) should be able to start the workflow and retry any failed steps. Finance (and only finance) should be able to resume at the input step.
548+
549+
```python
550+
@workflow("An expensive workflow", auth=allow_roles("ops"))
551+
def W(...):
552+
return begin >> A >> ... >> notify_finance >> approval >> ... >> Z
553+
554+
@inputstep("Approval", resume_auth=allow_roles("finance"), retry_auth=allow_roles("ops"))
555+
def approval(...):
556+
...
557+
```
558+
559+
560+
#### Hand-off Model
561+
!!!example
562+
Suppose we have two teams, Dev and Platform, and a long workflow W that should be handed off to Platform at step `approval`.
563+
564+
Dev can start the workflow and retry steps prior to S. Once step S is reached, Platform (and only Platform) can resume the workflow and retry later failed steps.
565+
566+
```python
567+
@workflow("An expensive workflow", auth=allow_roles("dev"))
568+
def W(...):
569+
return begin >> A >> ... >> notify_platform >> handoff >> ... >> Z
570+
571+
@inputstep("Hand-off", resume_auth=allow_roles("platform"))
572+
def handoff(...):
573+
...
574+
```
575+
Notice that default behaviors let us ignore `retry_auth` arguments in both decorators.
576+
577+
#### Restricted Retries Model
578+
!!!example
579+
Suppose we have a workflow that anyone can run, but with steps that should only be retried by users with certain backend access.
580+
581+
```python
582+
@workflow("A workflow for any user", retry_auth=allow_roles("admin"))
583+
def W(...):
584+
return begin >> A >> ... >> S >> ... >> Z
585+
```
586+
587+
Note that we could specify `auth=allow_roles("user")` if helpful, or we can omit `auth` to fail open to any logged in user.
588+
272589
[1]: https://github.com/workfloworchestrator/example-orchestrator-ui
273590
[2]: https://github.com/workfloworchestrator/example-orchestrator
274591
[3]: https://next-auth.js.org/

orchestrator/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
"""This is the orchestrator workflow engine."""
1515

16-
__version__ = "4.1.0rc1"
16+
__version__ = "4.1.0rc2"
1717

1818
from orchestrator.app import OrchestratorCore
1919
from orchestrator.settings import app_settings

0 commit comments

Comments
 (0)