docs: tweak permission tutorial step 4

Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
MT Lewis
2022-03-14 15:03:06 +00:00
committed by Joon Park
parent ddc965debe
commit ac976be7ee
@@ -6,10 +6,9 @@ description: Explains how to authorize access to paginated data in a Backstage p
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.
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.
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, something like this could be done:
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`:
```diff
router.get('/todos', async (req, res) => {
@@ -28,9 +27,46 @@ Potentially, something like this could be done:
});
```
This should do the trick. However, this approach has a downside. It would force us to retrieve all the elements upfront and authorize them one by one, and mean 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, and means that concerns like pagination that are handled by the data source today also need to move into the plugin.
To avoid this situation, the permissions framework has support for filtering items in the data source itself.
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.
> 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
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:
```diff
import { Permission } from '@backstage/plugin-permission-common';$$
export const TODO_LIST_RESOURCE_TYPE = 'todo-item';
export const todosListCreate: Permission = {
name: 'todos.list.create',
attributes: {
action: 'create',
},
};
export const todosListUpdate: Permission = {
name: 'todos.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,
+};
```
## Authorizing using the new permission
`plugins/todo-list-backend/src/service/router.ts`
@@ -47,23 +83,26 @@ To avoid this situation, the permissions framework has support for filtering ite
+ todosListRead,
TODO_LIST_RESOURCE_TYPE,
} from './permissions';
+ import { rules } from './rules';
router.get('/todos', async (req, res) => {
+ const token = getBearerTokenFromAuthorizationHeader(
+ req.header('authorization'),
+ );
+
+ const decision = (
+ await permissions.authorize([{ permission: todosListRead }], {
+ token,
+ })
+ )[0];
+
+ if (decision.result === AuthorizeResult.DENY) {
+ throw new NotAllowedError('Unauthorized');
+ }
+
+ if (decision.result === AuthorizeResult.CONDITIONAL) {
+ const conditionTransformer: ConditionTransformer<TodoFilter> =
+ createConditionTransformer(rules);
+ createConditionTransformer(Object.values(rules));
+ const filter = conditionTransformer(decision.conditions) as TodoFilter;
+ res.json(getAll(filter));
+ return;
@@ -73,23 +112,15 @@ To avoid this situation, the permissions framework has support for filtering ite
});
```
In this case, we are not passing any `resourceRef` when invoking `permissions.authorize()`.
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`.
Since there is no `resourceRef` and the permission policy is returning a conditional response, the permission framework can't make a decision
on its own and it's expecting the todo list backend's router to take care of this case.
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.
Instead of authorizing every todo item one by one, we can implement this more efficiently.
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.
If all the items were stored in a database, we could transform each permission result in the proper database query, letting the database do the filtering.
## Test the authorized read endpoint
If this approach is not applicable, you can still use the result-by-result authorization approach mentioned at the beginning of the section.
Fortunately, our todo service is smart enough and lets us pass an optional `filter` function when invoking `getAll()`.
This is exactly what the `toQuery` field in the permission rule does.
In this particular example, the `isOwner` rule returns a function (expected by `getAll` method). But there is no constraint regarding the shape of the returned object: any type of data can be returned.
Now 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's handler to return a conditional result whenever a todosListCreate permission is received. We could reuse the same result as the update action:
```diff
if (request.permission.resourceType === 'todo-item') {