Skip to content

Commit

Permalink
feat(orchestrator): add fine-grained RBAC
Browse files Browse the repository at this point in the history
The admin can limit access to specific workflows and instances by
workflow ID.

Signed-off-by: Marek Libra <mlibra@redhat.com>
  • Loading branch information
mareklibra committed Nov 22, 2024
1 parent e9b2878 commit 86ee818
Show file tree
Hide file tree
Showing 11 changed files with 416 additions and 86 deletions.
114 changes: 80 additions & 34 deletions plugins/orchestrator-backend/src/service/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import type { Config } from '@backstage/config';
import type { DiscoveryApi } from '@backstage/core-plugin-api';
import {
AuthorizePermissionResponse,
AuthorizeResult,
BasicPermission,
} from '@backstage/plugin-permission-common';
Expand All @@ -29,10 +30,15 @@ import {
openApiDocument,
orchestratorPermissions,
orchestratorWorkflowExecutePermission,
orchestratorWorkflowExecuteSpecificPermission,
orchestratorWorkflowInstanceAbortPermission,
orchestratorWorkflowInstanceAbortSpecificPermission,
orchestratorWorkflowInstanceReadPermission,
orchestratorWorkflowInstanceReadSpecificPermission,
orchestratorWorkflowInstancesReadPermission,
orchestratorWorkflowReadPermission,
orchestratorWorkflowReadSpecificPermission,
orchestratorWorkflowsReadPermission,
QUERY_PARAM_BUSINESS_KEY,
QUERY_PARAM_INCLUDE_ASSESSMENT,
} from '@janus-idp/backstage-plugin-orchestrator-common';
Expand Down Expand Up @@ -62,17 +68,29 @@ interface RouterApi {

const authorize = async (
request: HttpRequest,
permission: BasicPermission,
anyOfPermissions: BasicPermission[],
permissionsSvc: PermissionsService,
httpAuth: HttpAuthService,
) => {
const decision = (
await permissionsSvc.authorize([{ permission: permission }], {
credentials: await httpAuth.credentials(request),
})
)[0];

return decision;
): Promise<AuthorizePermissionResponse> => {
const credentials = await httpAuth.credentials(request);

const decisionResponses: AuthorizePermissionResponse[][] = await Promise.all(
anyOfPermissions.map(permission =>
permissionsSvc.authorize([{ permission }], {
credentials,
}),
),
);
const decisions: AuthorizePermissionResponse[] = decisionResponses.map(
d => d[0],
);

const allow = decisions.find(d => d.result === AuthorizeResult.ALLOW);
return (
allow || {
result: AuthorizeResult.DENY,
}
);
};

export async function createBackendRouter(
Expand Down Expand Up @@ -296,7 +314,7 @@ function setupInternalRoutes(
});
const decision = await authorize(
req,
orchestratorWorkflowInstancesReadPermission,
[orchestratorWorkflowsReadPermission],
permissions,
httpAuth,
);
Expand Down Expand Up @@ -332,7 +350,10 @@ function setupInternalRoutes(

const decision = await authorize(
_req,
orchestratorWorkflowReadPermission,
[
orchestratorWorkflowReadPermission,
orchestratorWorkflowReadSpecificPermission(workflowId),
],
permissions,
httpAuth,
);
Expand Down Expand Up @@ -369,7 +390,10 @@ function setupInternalRoutes(

const decision = await authorize(
req,
orchestratorWorkflowExecutePermission,
[
orchestratorWorkflowExecutePermission,
orchestratorWorkflowExecuteSpecificPermission(workflowId),
],
permissions,
httpAuth,
);
Expand Down Expand Up @@ -451,7 +475,10 @@ function setupInternalRoutes(

const decision = await authorize(
_req,
orchestratorWorkflowReadPermission,
[
orchestratorWorkflowReadPermission,
orchestratorWorkflowReadSpecificPermission(workflowId),
],
permissions,
httpAuth,
);
Expand Down Expand Up @@ -485,7 +512,7 @@ function setupInternalRoutes(
});
const decision = await authorize(
_req,
orchestratorWorkflowInstanceReadPermission,
[orchestratorWorkflowInstancesReadPermission],
permissions,
httpAuth,
);
Expand Down Expand Up @@ -521,7 +548,10 @@ function setupInternalRoutes(
});
const decision = await authorize(
req,
orchestratorWorkflowInstanceReadPermission,
[
orchestratorWorkflowReadPermission,
orchestratorWorkflowReadSpecificPermission(workflowId),
],
permissions,
httpAuth,
);
Expand Down Expand Up @@ -640,7 +670,7 @@ function setupInternalRoutes(

const decision = await authorize(
req,
orchestratorWorkflowInstancesReadPermission,
[orchestratorWorkflowInstancesReadPermission],
permissions,
httpAuth,
);
Expand Down Expand Up @@ -675,7 +705,7 @@ function setupInternalRoutes(

const decision = await authorize(
req,
orchestratorWorkflowInstancesReadPermission,
[orchestratorWorkflowInstancesReadPermission],
permissions,
httpAuth,
);
Expand Down Expand Up @@ -709,26 +739,39 @@ function setupInternalRoutes(
message: `Received request to '${endpoint}' endpoint`,
});

const decision = await authorize(
_req,
orchestratorWorkflowInstanceReadPermission,
permissions,
httpAuth,
);
if (decision.result === AuthorizeResult.DENY) {
manageDenyAuthorization(endpointName, endpoint, _req);
}
const includeAssessment = routerApi.v2.extractQueryParam(
c.request,
QUERY_PARAM_INCLUDE_ASSESSMENT,
);
return routerApi.v2
.getInstanceById(instanceId, !!includeAssessment)
.then(result => res.status(200).json(result))
.catch(error => {
auditLogRequestError(error, endpointName, endpoint, _req);
next(error);
});

try {
const assessedInstance = await routerApi.v2.getInstanceById(
instanceId,
!!includeAssessment,
);

const workflowId = assessedInstance.instance.processId;

// we need to authorize after retrieval to know the workflowId
const decision = await authorize(
_req,
[
orchestratorWorkflowInstanceReadPermission,
orchestratorWorkflowInstanceReadSpecificPermission(workflowId),
],
permissions,
httpAuth,
);
if (decision.result === AuthorizeResult.DENY) {
manageDenyAuthorization(endpointName, endpoint, _req);
}

return res.status(200).json(assessedInstance);
} catch (error) {
auditLogRequestError(error, endpointName, endpoint, _req);
next(error);
throw error;
}
},
);

Expand All @@ -751,7 +794,10 @@ function setupInternalRoutes(

const decision = await authorize(
_req,
orchestratorWorkflowInstanceAbortPermission,
[
orchestratorWorkflowInstanceAbortPermission,
// TODO: get workflowId and use orchestratorWorkflowInstanceAbortSpecificPermission(workflowId)
],
permissions,
httpAuth,
);
Expand Down
49 changes: 49 additions & 0 deletions plugins/orchestrator-common/src/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,72 @@ export const orchestratorWorkflowInstanceReadPermission = createPermission({
},
});

/**
* @param workflowId Mind this is workflowId and not instanceId
*/
export const orchestratorWorkflowInstanceReadSpecificPermission = (
workflowId: string,
) =>
createPermission({
name: `orchestrator.workflowInstance.read.${workflowId}`,
attributes: {
action: 'read',
},
});

export const orchestratorWorkflowsReadPermission = createPermission({
name: 'orchestrator.workflows.read',
attributes: {
action: 'read',
},
});

export const orchestratorWorkflowReadPermission = createPermission({
name: 'orchestrator.workflow.read',
attributes: {
action: 'read',
},
});

export const orchestratorWorkflowReadSpecificPermission = (
workflowId: string,
) =>
createPermission({
name: `orchestrator.workflow.read.${workflowId}`,
attributes: {
action: 'read',
},
});

export const orchestratorWorkflowExecutePermission = createPermission({
name: 'orchestrator.workflow.execute',
attributes: {},
});

export const orchestratorWorkflowExecuteSpecificPermission = (
workflowId: string,
) =>
createPermission({
name: `orchestrator.workflow.execute.${workflowId}`,
attributes: {},
});

export const orchestratorWorkflowInstanceAbortPermission = createPermission({
name: 'orchestrator.workflowInstance.abort',
attributes: {},
});

/**
* @param workflowId Mind this is workflowId and not instanceId
*/
export const orchestratorWorkflowInstanceAbortSpecificPermission = (
workflowId: string,
) =>
createPermission({
name: `orchestrator.workflowInstance.abort.${workflowId}`,
attributes: {},
});

export const orchestratorPermissions = [
orchestratorWorkflowReadPermission,
orchestratorWorkflowExecutePermission,
Expand Down
7 changes: 7 additions & 0 deletions plugins/orchestrator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ For more information about the configuration options, including other optional p

The `orchestrator` plugin includes an extensible form for executing forms. For detailed guidance see the [Extensible Workflow Execution Form Documentation](https://github.com/janus-idp/backstage-plugins/blob/main/plugins/orchestrator/docs/extensibleForm.md).

### Setting up permissions

The HTTP endpoints exposed by the `orchestrator-backend` can enforce authorization if the [RBAC plugin](https://github.com/backstage/community-plugins/tree/main/workspaces/rbac/plugins) is deployed.
Please refer the RBAC plugin documentation for the setup steps (mind they rely on the [Backstage authentication and identity](https://backstage.io/docs/auth)).

More detailed info about permissions can be found in [docs/Permissions.md](docs/Permissions.md).

## For users

### Using the Orchestrator plugin in Backstage
Expand Down
44 changes: 34 additions & 10 deletions plugins/orchestrator/docs/Permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,54 @@ the RBAC plugin. The result is control over what users can see or execute.

## Orchestrator Permissions

| Name | Resource Type | Policy | Description | Requirements |
| ----------------------------------- | -------------- | ------ | ----------------------------------------------------------------- | ------------ |
| orchestrator.workflowInstances.read | named resource | read | Allows the user to read orchestrator workflows overview | |
| orchestrator.workflowInstance.read | named resource | read | Allows the user to read the details of a single workflow instance | |
| orchestrator.workflowInstance.abort | named resource | use | Allows the user to abort a workflow instance | |
| orchestrator.workflow.read | named resource | read | Allows the user to read the workflow definitions | |
| orchestrator.workflow.execute | named resource | use | Allows the user to execute a workflow | |
| Name | Resource Type | Policy | Description | Requirements |
| -------------------------------------------------- | -------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------- | ------------ |
| orchestrator.workflowInstances.read | named resource | read | Allows the user to read **all of orchestrator workflow runs** (workflow instances) | |
| orchestrator.workflowInstance.read | named resource | read | Allows the user to read the details of **any single** workflow instance | |
| orchestrator.workflowInstance.read.[`workflowId`] | named resource | read | Allows the user to read the details of `workflowId`-workflow's instances (replace the `[workflowId]` by a single workflow ID) | |
| orchestrator.workflowInstance.abort | named resource | use | Allows the user to abort any workflow instance | |
| orchestrator.workflowInstance.abort.[`workflowId`] | named resource | use | Allows the user to abort a single workflow instance | |
| orchestrator.workflows.read | named resource | read | Allows the user to read **all of workflows** (but not their instances) | |
| orchestrator.workflow.read | named resource | read | Allows the user to read the workflow definitions | |
| orchestrator.workflow.read.[`workflowId`] | named resource | read | Allows the user to read the details of a single workflow definition | |
| orchestrator.workflow.execute | named resource | use | Allows the user to execute a workflow | |
| orchestrator.workflow.execute.[`workflowId`] | named resource | use | Allows the user to execute a single workflow | |

The user is permitted to do an action if either the generic permission or the specific one allows it.
In other words, it is not possible to grant generic `orchestrator.workflowInstance.read` and then selectively disable it for a specific workflow via `orchestrator.workflowInstance.read.[workflowId]` with `deny`.

## Policy File

To get started with policies, we recommend defining 2 roles and assigning them to groups or users.

See the example [policy file](./rbac-policy.csv)
As an example, mind the following [policy file](./rbac-policy.csv).

Since the `guest` user has the `default/workflowViewer` role, it can:

- view the list of workflows (`orchestrator.workflows.read`)
- view any workflow details (`orchestrator.workflow.read`)
- view the list of all instances (`orchestrator.workflowInstances.read`)
- view any instance (`orchestrator.workflowInstance.read`)
- execute just the `yamlgreet` workflow but not any other (`orchestrator.workflow.execute.yamlgreet`)

The users of the `workflowAdmin` role have full permissions.

```csv
p, role:default/workflowViewer, orchestrator.workflows.read, read, allow
p, role:default/workflowViewer, orchestrator.workflow.read, read, allow
p, role:default/workflowViewer, orchestrator.workflowInstances.read, read, allow
p, role:default/workflowViewer, orchestrator.workflowInstance.read, read, allow
p, role:default/workflowViewer, orchestrator.workflow.execute.yamlgreet, use, allow
p, role:default/workflowAdmin, orchestrator.workflows.read, read, allow
p, role:default/workflowAdmin, orchestrator.workflow.read, read, allow
p, role:default/workflowAdmin, orchestrator.workflow.execute, use, allow
p, role:default/workflowAdmin, orchestrator.workflowInstance.abort, use, deny
p, role:default/workflowAdmin, orchestrator.workflowInstance.abort, use, allow
p, role:default/workflowAdmin, orchestrator.workflowInstances.read, read, allow
p, role:default/workflowAdmin, orchestrator.workflowInstance.read, read, allow
p, role:default/workflowAdmin, orchestrator.workflow.execute, use, allow
g, user:default/guest, role:default/workflowViewer
g, user:default/myOrgUser, role:default/workflowAdmin
g, group:default/platformAdmins, role:default/worflowAdmin
Expand Down
18 changes: 15 additions & 3 deletions plugins/orchestrator/docs/rbac-policy.csv
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
p, role:default/workflowViewer, orchestrator.workflows.read, read, allow
p, role:default/workflowViewer, orchestrator.workflow.read, read, allow

p, role:default/workflowViewer, orchestrator.workflowInstances.read, read, allow
p, role:default/workflowViewer, orchestrator.workflowInstance.read, read, allow
#p, role:default/workflowViewer, orchestrator.workflowInstance.read, read, allow
p, role:default/workflowViewer, orchestrator.workflowInstance.read.yamlgreet, read, allow

#p, role:default/workflowViewer, orchestrator.workflow.execute, use, allow
p, role:default/workflowViewer, orchestrator.workflow.execute.yamlgreet, use, allow
#p, role:default/workflowViewer, orchestrator.workflow.execute.wait-or-error, use, allow

p, role:default/workflowAdmin, orchestrator.workflows.read, read, allow
p, role:default/workflowAdmin, orchestrator.workflow.read, read, allow
p, role:default/workflowAdmin, orchestrator.workflow.execute, use, allow
p, role:default/workflowAdmin, orchestrator.workflowInstance.abort, use, allow
p, role:default/workflowAdmin, orchestrator.workflowInstances.read, read, allow
p, role:default/workflowAdmin, orchestrator.workflowInstance.read, read, allow

g, user:default/guest, role:default/workflowViewer
#p, role:default/workflowAdmin, orchestrator.workflow.execute, use, allow
p, role:default/workflowAdmin, orchestrator.workflow.execute.yamlgreet, use, allow

g, user:development/guest, role:default/workflowViewer
g, user:default/rgolangh, role:default/workflowAdmin
g, user:default/mareklibra, role:default/workflowAdmin
Loading

0 comments on commit 86ee818

Please sign in to comment.