Sync permission docs with framework changes

Signed-off-by: Joe Porpeglia <josephp@spotify.com>
This commit is contained in:
Joe Porpeglia
2022-04-19 18:43:50 -04:00
committed by Joon Park
parent 480dffcfef
commit f70e9b5368
4 changed files with 75 additions and 78 deletions
@@ -105,10 +105,6 @@ Now if you start your application you should be able to reach the `/todo-list` p
---
// TODO(vinzscam): split the tutorial in two parts:
// - plugins relying on external entities
// - plugins owning their own entities
## Integrate the new plugin
If you play with the UI, you will notice that it is possible to perform a few actions:
@@ -155,14 +155,13 @@ In order to test the logic above, the integrators of your backstage instance nee
Now the frontend should show an error whenever you try to create a new Todo item.
Let's flip the result back to `ALLOW`:
Let's flip the result back to `ALLOW` before moving on.
```diff
if (request.permission.attributes.action === 'create') {
return {
- result: AuthorizeResult.DENY,
+ result: AuthorizeResult.ALLOW,
};
if (isPermission(request.permission, todoListCreate)) {
return {
- result: AuthorizeResult.DENY,
+ result: AuthorizeResult.ALLOW,
};
}
```
Now the create endpoint should be enabled again.
@@ -6,9 +6,6 @@ description: Explains how to add a resource permission check to a Backstage plug
When performing updates (or other operations) on specific [resources](../concepts.md#resources-and-rules), the permissions framework allows for the decision to be based on characteristics of the resource itself. This means that it's possible to write policies that (for example) allow the operation for users that own a resource, and deny the operation otherwise.
// TODO(vinzscam): remind that the plugin used in this tutorial is bringing its own types to backstage.
// for plugins relying on external entities (like catalog entities) please follow [link] tutorial.
## Creating the update permission
Let's add a new permission to the file `plugins/todo-list-common/src/permissions.ts` from [the previous section](./02-adding-a-basic-permission-check.md).
@@ -156,9 +153,9 @@ Now, let's create the new endpoint by editing `plugins/todo-list-backend/src/ser
Now let's go back to the permission policy's handle function and try to authorize our new permission.
Let's edit `packages/backend/src/plugins/permission.ts`:
```diff
// packages/backend/src/plugins/permission.ts
import {
BackstageIdentityResponse,
IdentityClient
@@ -179,9 +176,10 @@ Let's edit `packages/backend/src/plugins/permission.ts`:
if (isPermission(request.permission, todoListCreate)) {
return {
result: AuthorizeResult.DENY,
result: AuthorizeResult.ALLOW,
};
}
+ if (isPermission(request.permission, todoListUpdate)) {
+ return {
+ result: AuthorizeResult.CONDITIONAL,
@@ -193,6 +191,7 @@ Let's edit `packages/backend/src/plugins/permission.ts`:
+ },
+ };
+ }
+
return {
result: AuthorizeResult.ALLOW,
};
@@ -4,11 +4,9 @@ title: 4. Authorizing access to paginated data
description: Explains how to authorize access to paginated data in a Backstage plugin
---
Authorizing `GET /todos` is similar to the update endpoint, in that it should be possible to authorize read access to todo entries based on their characteristics. When a `GET /todos` request is received, only the items that the user is permitted to see should be returned.
Authorizing `GET /todos` is similar to the update endpoint, in that it should be possible to authorize access based on the characteristics of each resource. However, we'll need to authorize a list of resources for this endpoint.
As in the previous case, the permission policy can't take the decision itself, meaning that a conditional decision should be returned. However, this time rather than a single `resourceRef`, we have a whole list of resources to authorize.
Potentially, we could implement something like the below, leveraging the batching functionality to authorize all of the todos, and then returning only the ones for which the decision was `ALLOW`:
One possible solution may leverage the batching functionality to authorize all of the todos, and then returning only the ones for which the decision was `ALLOW`:
```diff
router.get('/todos', async (req, res) => {
@@ -27,50 +25,48 @@ Potentially, we could implement something like the below, leveraging the batchin
});
```
This approach will work for simple cases, but it has a downside: it forces us to retrieve all the elements upfront and authorize them one by one, and means that concerns like pagination that are handled by the data source today also need to move into the plugin.
This approach will work for simple cases, but it has a downside: it forces us to retrieve all the elements upfront and authorize them one by one. This forces the plugin implementation to handle concerns like pagination, which is currently handled by the data source.
To avoid this situation, the permissions framework has support for filtering items in the data source itself. In this tutorial, we'll create a permission for reading todo items, and use it to filter out unauthorized todo items in the data store itself.
To avoid this situation, the permissions framework has support for filtering items in the data source itself. In this part of the tutorial, we'll describe the steps required to use that behavior.
> Note: in order to perform authorization filtering in this way, the data source must allow filters to be logically combined with AND, OR, and NOT operators. The conditional decisions returned by the permissions framework use a [nested object](https://backstage.io/docs/reference/plugin-permission-common.permissioncriteria) to combine conditions. If you're implementing a filter API from scratch, we recommend using the same shape for ease of interoperability. If not, you'll need to implement a function which transforms the nested object into your own format.
## Creating a new permission
## Creating the read permission
Let's add another permission to the file `plugins/todo-list-backend/src/service/permissions.ts`, with the same structure as the existing `todosListUpdate` permission:
Let's add another permission to for the plugin.
```diff
import { Permission } from '@backstage/plugin-permission-common';$$
// plugins/todo-list-backend/src/service/permissions.ts
export const TODO_LIST_RESOURCE_TYPE = 'todo-item';
import { createPermission } from '@backstage/plugin-permission-common';
export const todosListCreate: Permission = {
name: 'todos.list.create',
attributes: {
action: 'create',
},
};
export const TODO_LIST_RESOURCE_TYPE = 'todo-item';
export const todosListUpdate: Permission = {
name: 'todos.list.update',
attributes: {
action: 'update',
},
resourceType: TODO_LIST_RESOURCE_TYPE,
};
export const todoListCreate = createPermission({
name: 'todo.list.create',
attributes: { action: 'create' },
});
export const todoListUpdate = createPermission({
name: 'todo.list.update',
attributes: { action: 'update' },
resourceType: TODO_LIST_RESOURCE_TYPE,
});
+
+export const todosListRead: Permission = {
+ name: 'todos.list.read',
+ attributes: {
+ action: 'read',
+ },
+ resourceType: TODO_LIST_RESOURCE_TYPE,
+};
+ export const todosListRead = createPermission({
+ name: 'todos.list.read',
+ attributes: { action: 'read' },
+ resourceType: TODO_LIST_RESOURCE_TYPE,
+ });
```
## Authorizing using the new permission
## Using conditional policy decisions
`plugins/todo-list-backend/src/service/router.ts`
So far we've only used the `PermissionEvaluator.authorize` method, which will evaluate conditional decisions before returning a result. In this step, we want to evaluate conditional decisions within our plugin, so we'll use `PermissionEvaluator.authorizeConditional` instead.
```diff
// plugins/todo-list-backend/src/service/router.ts
- import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
+ import {
+ createPermissionIntegrationRouter,
@@ -87,13 +83,15 @@ export const todosListUpdate: Permission = {
} from './permissions';
+ import { rules } from './rules';
+ const transformConditions: ConditionTransformer<TodoFilter> = createConditionTransformer(Object.values(rules));
router.get('/todos', async (req, res) => {
+ const token = getBearerTokenFromAuthorizationHeader(
+ req.header('authorization'),
+ );
+
+ const decision = (
+ await permissions.authorize([{ permission: todosListRead }], {
+ await permissions.authorizeConditional([{ permission: todosListRead }], {
+ token,
+ })
+ )[0];
@@ -103,9 +101,7 @@ export const todosListUpdate: Permission = {
+ }
+
+ if (decision.result === AuthorizeResult.CONDITIONAL) {
+ const conditionTransformer: ConditionTransformer<TodoFilter> =
+ createConditionTransformer(Object.values(rules));
+ const filter = conditionTransformer(decision.conditions) as TodoFilter;
+ const filter = transformConditions(decision.conditions);
+ res.json(getAll(filter));
+ } else {
+ res.json(getAll());
@@ -115,36 +111,43 @@ export const todosListUpdate: Permission = {
});
```
In this case, we are not passing a `resourceRef` when invoking `permissions.authorize()`. Since there is no `resourceRef`, the permission framework can't apply conditions, and instead returns conditional decisions all the way back to the caller - in this case, the `todo-list-backend`.
To make the process of handling conditional decisions easier, the permission framework provides a `createConditionTransformer` helper. This function accepts an array of permission rules, and returns a transformer function which converts the conditions to the format needed by the plugin using the `toQuery` method defined on each rule.
To make the process of handling conditional decisions easier, the permission framework provides a `createConditionTransformer` helper. This function accepts an array of permission rules, and returns a transformer function which converts the conditions in conditional decisions to the format needed by the plugin using the `toQuery` method on permission rules.
Since the todo api groups filters using the same nested object structure as the permission framework, we can pass the output of our condition transformer straight to it. If the filters were grouped differently, we'd need to transform it at this point before passing it to the api.
Since `TodoFilter` used in our plugin matches the structure of the conditions object, we can directly pass the output of our condition transformer. If the filters were structured differently, we'd need to transform it further before passing it to the api.
## Test the authorized read endpoint
Let's update our permission policy's handler to return a conditional result whenever a todosListCreate permission is received. We could reuse the same result as the update action:
Let's update our permission policy to return a conditional result whenever a `todosListRead` permission is received. In this case, we can reuse the decision returned for the `todosListCreate` permission.
```diff
if (request.permission.resourceType === 'todo-item') {
- if (request.permission.resourceType === 'todo-item') {
+ if (
+ request.permission.attributes.action === 'update' ||
+ request.permission.attributes.action === 'read'
+ ) {
return {
result: AuthorizeResult.CONDITIONAL,
pluginId: 'todolist',
resourceType: 'todo-item',
conditions: {
rule: 'IS_OWNER',
params: [user?.identity.userEntityRef],
},
};
}
// packages/backend/src/plugins/permission.ts
...
import {
todoListCreate,
todoListUpdate,
+ todoListRead,
TODO_LIST_RESOURCE_TYPE,
} from '@internal/plugin-todo-list-common';
...
- if (isPermission(request.permission, todoListUpdate)) {
+ if (
+ isPermission(request.permission, todoListUpdate) ||
+ isPermission(request.permission, todoListRead)
+ ) {
return {
result: AuthorizeResult.CONDITIONAL,
pluginId: 'todolist',
resourceType: TODO_LIST_RESOURCE_TYPE,
conditions: {
rule: 'IS_OWNER',
params: [user?.identity.userEntityRef],
},
};
}
```
Once the changes to the permission policy are saved, the UI should should show only the items you have created.
// TODO(vinzscam): add frontend documentation
Once the changes to the permission policy are saved, the UI should should show only the todo items you've created.