diff --git a/contrib/plugins/todo-list-common/.eslintrc.js b/contrib/plugins/todo-list-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/contrib/plugins/todo-list-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/contrib/plugins/todo-list-common/README.md b/contrib/plugins/todo-list-common/README.md new file mode 100644 index 0000000000..3dc43d8dd9 --- /dev/null +++ b/contrib/plugins/todo-list-common/README.md @@ -0,0 +1,13 @@ +# todo-list-common + +Welcome to the todo-list-common plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/todo-list-common](http://localhost:3000/todo-list-common). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/contrib/plugins/todo-list-common/package.json b/contrib/plugins/todo-list-common/package.json new file mode 100644 index 0000000000..6ea0334388 --- /dev/null +++ b/contrib/plugins/todo-list-common/package.json @@ -0,0 +1,46 @@ +{ + "name": "@internal/plugin-todo-list-common", + "version": "1.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "^0.9.3-next.0", + "@backstage/core-plugin-api": "^1.0.0", + "@backstage/plugin-permission-common": "^0.6.0-next.0", + "@backstage/theme": "^0.2.15" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.17.0-next.1", + "@backstage/core-app-api": "^1.0.1-next.0", + "@backstage/dev-utils": "^1.0.1-next.0", + "@backstage/test-utils": "^1.0.1-next.1", + "@types/node": "*", + "msw": "^0.35.0", + "cross-fetch": "^3.1.5" + }, + "files": [ + "dist" + ] +} diff --git a/contrib/plugins/todo-list-common/src/index.ts b/contrib/plugins/todo-list-common/src/index.ts new file mode 100644 index 0000000000..aff3b136bd --- /dev/null +++ b/contrib/plugins/todo-list-common/src/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './permissions'; diff --git a/contrib/plugins/todo-list-common/src/permissions.ts b/contrib/plugins/todo-list-common/src/permissions.ts new file mode 100644 index 0000000000..a91cafad0e --- /dev/null +++ b/contrib/plugins/todo-list-common/src/permissions.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createPermission } from '@backstage/plugin-permission-common'; + +export const tempExamplePermission = createPermission({ + name: 'temp.example.noop', + attributes: {}, +}); diff --git a/docs/permission/plugin-authors/01-setup.md b/docs/permission/plugin-authors/01-setup.md index 6114fbd63f..c09f11cdbf 100644 --- a/docs/permission/plugin-authors/01-setup.md +++ b/docs/permission/plugin-authors/01-setup.md @@ -10,30 +10,34 @@ The rest of this page is focused on adding the `todo-list` and `todo-list-backen ## Setup for the Tutorial -We will use a "Todo list" feature, composed of the `todo-list` and `todo-list-backend` plugins. +We will use a "Todo list" feature, composed of the `todo-list` and `todo-list-backend` plugins, as well as their dependency, `todo-list-common`. The source code is available here: - [todo-list](https://github.com/backstage/backstage/blob/master/contrib/plugins/todo-list) - [todo-list-backend](https://github.com/backstage/backstage/blob/master/contrib/plugins/todo-list-backend) +- [todo-list-common](https://github.com/backstage/backstage/blob/master/contrib/plugins/todo-list-common) -1. Copy-paste the two folders into the plugins folder of your backstage application repository or run the following script from the root of your backstage application: +1. Copy-paste the three folders into the plugins folder of your backstage application repository or run the following script from the root of your backstage application: ```bash $ curl https://codeload.github.com/backstage/backstage/zip/refs/heads/master | \ tar -C plugins --strip-components=1 -xv \ backstage-master/contrib/plugins/todo-list \ - backstage-master/contrib/plugins/todo-list-backend + backstage-master/contrib/plugins/todo-list-backend \ + backstage-master/contrib/plugins/todo-list-common ``` Your application structure should look something like this: + // TODO: UPDATE THIS IMAGE + ![backstage application files structure](../../assets/permission/permission-tutorial-backstage-application-initial-structure.png) 2. add the frontend and backend plugins as dependencies of your Backstage app and backend respectively: ``` - $ yarn workspace backend add @internal/plugin-todo-list-backend@^1.0.0 + $ yarn workspace backend add @internal/plugin-todo-list-backend@^1.0.0 @internal/plugin-todo-list-common@^1.0.0 $ yarn workspace app add @internal/plugin-todo-list@^1.0.0 ``` diff --git a/docs/permission/plugin-authors/02-adding-a-basic-permission-check.md b/docs/permission/plugin-authors/02-adding-a-basic-permission-check.md index b561fd8059..55dd3e85ab 100644 --- a/docs/permission/plugin-authors/02-adding-a-basic-permission-check.md +++ b/docs/permission/plugin-authors/02-adding-a-basic-permission-check.md @@ -10,32 +10,33 @@ For this tutorial, we'll use a basic permission check to authorize the `create` We'll start by creating a new permission, and then we'll use the permission api to call `authorize` with it during todo creation. -## Setup +## Creating a new permission + +Let's navigate to the file `plugins/todo-list-common/src/permissions.ts` and add our first permission: + +```diff + import { createPermission } from '@backstage/plugin-permission-common'; + +- export const tempExamplePermission = createPermission({ +- name: 'temp.example.noop', +- attributes: {}, ++ export const todoListCreate = createPermission({ ++ name: 'todo.list.create', ++ attributes: { action: 'create' }, + }); +``` + +For this tutorial, we've automatically exported all permissions from this file (see `plugins/todo-list-common/src/index.ts`). For the actual plugins that you author, we recommend exporting all permissions from your plugin, so that Backstage integrators can import them when writing policies. + +## Authorizing using the new permission Install the following module: ``` $ yarn workspace @internal/plugin-todo-list-backend \ - add @backstage/plugin-permission-common + add @backstage/plugin-permission-common @internal/plugin-todo-list-common ``` -## Creating a new permission - -Let's create a new file `plugins/todo-list-backend/src/service/permissions.ts` with the following content: - -```typescript -import { createPermission } from '@backstage/plugin-permission-common'; - -export const todosListCreate = createPermission({ - name: 'todos.list.create', - attributes: { action: 'create' }, -}); -``` - -We recommend exporting all permissions from your plugin, so that Backstage integrators can import them when writing policies. - -## Authorizing using the new permission - Edit `plugins/todo-list-backend/src/service/router.ts`: ```diff @@ -44,7 +45,7 @@ Edit `plugins/todo-list-backend/src/service/router.ts`: - import { InputError } from '@backstage/errors'; + import { InputError, NotAllowedError } from '@backstage/errors'; + import { PermissionEvaluator, AuthorizeResult } from '@backstage/plugin-permission-common'; -+ import { todosListCreate } from './permissions'; ++ import { todoListCreate } from '@internal/plugin-todo-list-common'; ... @@ -69,7 +70,7 @@ Edit `plugins/todo-list-backend/src/service/router.ts`: const user = token ? await identity.authenticate(token) : undefined; author = user?.identity.userEntityRef; + const decision = ( -+ await permissions.authorize([{ permission: todosListCreate }], { ++ await permissions.authorize([{ permission: todoListCreate }], { + token, + }) + )[0]; @@ -132,7 +133,7 @@ In order to test the logic above, the integrators of your backstage instance nee + PolicyQuery, } from '@backstage/plugin-permission-node'; + import { isPermission } from '@backstage/plugin-permission-common'; -+ import { todosListCreate } from '@internal/plugin-todo-list-backend'; ++ import { todoListCreate } from '@internal/plugin-todo-list-common'; class TestPermissionPolicy implements PermissionPolicy { - async handle(): Promise { @@ -140,7 +141,7 @@ In order to test the logic above, the integrators of your backstage instance nee + request: PolicyQuery, + user?: BackstageIdentityResponse, + ): Promise { -+ if (isPermission(request.permission, todosListCreate)) { ++ if (isPermission(request.permission, todoListCreate)) { + return { + result: AuthorizeResult.DENY, + }; diff --git a/docs/permission/plugin-authors/03-adding-a-resource-permission-check.md b/docs/permission/plugin-authors/03-adding-a-resource-permission-check.md index e3ba40d0c2..583d7e7707 100644 --- a/docs/permission/plugin-authors/03-adding-a-resource-permission-check.md +++ b/docs/permission/plugin-authors/03-adding-a-resource-permission-check.md @@ -11,34 +11,34 @@ When performing updates (or other operations) on specific [resources](../concept ## Creating the update permission -Let's add a new permission to the file `plugins/todo-list-backend/src/service/permissions.ts` from [the previous section](./02-adding-a-basic-permission-check.md). +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). ```diff import { createPermission } from '@backstage/plugin-permission-common'; + export const TODO_LIST_RESOURCE_TYPE = 'todo-item'; + - export const todosListCreate = createPermission({ - name: 'todos.list.create', + export const todoListCreate = createPermission({ + name: 'todo.list.create', attributes: { action: 'create' }, }); + -+ export const todosListUpdate = createPermission({ -+ name: 'todos.list.update', ++ export const todoListUpdate = createPermission({ ++ name: 'todo.list.update', + attributes: { action: 'update' }, + resourceType: TODO_LIST_RESOURCE_TYPE, + }); ``` -Notice that unlike `todosListCreate`, the `todosListUpdate` permission contains a `resourceType` field. This field indicates to the permission framework that this permission is intended to be authorized in the context of a resource with type `'todo-item'`. You can use whatever string you like as the resource type, as long as you use the same value consistently for each type of resource. +Notice that unlike `todoListCreate`, the `todoListUpdate` permission contains a `resourceType` field. This field indicates to the permission framework that this permission is intended to be authorized in the context of a resource with type `'todo-item'`. You can use whatever string you like as the resource type, as long as you use the same value consistently for each type of resource. ## Setting up authorization for the update permission To start, let's edit `plugins/todo-list-backend/src/service/router.ts` in the same manner as we did in the previous section: ```diff -- import { todosListCreate } from './permissions'; -+ import { todosListCreate, todosListUpdate } from './permissions'; +- import { todoListCreate } from '@internal/plugin-todo-list-common'; ++ import { todoListCreate, todoListUpdate } from '@internal/plugin-todo-list-common'; ... @@ -52,7 +52,7 @@ To start, let's edit `plugins/todo-list-backend/src/service/router.ts` in the sa } + const decision = ( + await permissions.authorize( -+ [{ permission: todosListUpdate, resourceRef: req.body.id }], ++ [{ permission: todoListUpdate, resourceRef: req.body.id }], + { + token, + }, @@ -83,6 +83,7 @@ Create a new `plugins/todo-list-backend/src/service/rules.ts` file and append th ```typescript import { makeCreatePermissionRule } from '@backstage/plugin-permission-node'; +import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common'; import { Todo, TodoFilter } from './todos'; const createTodoListPermissionRule = makeCreatePermissionRule< @@ -93,7 +94,7 @@ const createTodoListPermissionRule = makeCreatePermissionRule< export const isOwner = createTodoListPermissionRule({ name: 'IS_OWNER', description: 'Should allow only if the todo belongs to the user', - resourceType: 'todo-item', + resourceType: TODO_LIST_RESOURCE_TYPE, apply: (resource: Todo, userId: string) => { return resource.author === userId; }, @@ -122,10 +123,12 @@ Now, let's create the new endpoint by editing `plugins/todo-list-backend/src/ser - `rules`: an array of all the permission rules you want to support in conditional decisions. ```diff -+ import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; +... + - import { add, getAll, update } from './todos'; + import { add, getAll, getTodo, update } from './todos'; -+ import { TODO_LIST_RESOURCE_TYPE } from './permissions'; ++ import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; ++ import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common'; + import { rules } from './rules'; export async function createRouter( @@ -165,21 +168,21 @@ Let's edit `packages/backend/src/plugins/permission.ts`: PolicyQuery, } from '@backstage/plugin-permission-node'; import { isPermission } from '@backstage/plugin-permission-common'; -- import { todosListCreate } from '@internal/plugin-todo-list'; +- import { todoListCreate } from '@internal/plugin-todo-list-common'; + import { -+ todosListCreate, -+ todosListUpdate, ++ todoListCreate, ++ todoListUpdate, + TODO_LIST_RESOURCE_TYPE, -+ } from '@internal/plugin-todo-list'; ++ } from '@internal/plugin-todo-list-common'; ... - if (isPermission(request.permission, todosListCreate)) { + if (isPermission(request.permission, todoListCreate)) { return { result: AuthorizeResult.DENY, }; } -+ if (isPermission(request.permission, todosListUpdate)) { ++ if (isPermission(request.permission, todoListUpdate)) { + return { + result: AuthorizeResult.CONDITIONAL, + pluginId: 'todolist', diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 4cf2c02e55..03e5cf5c3b 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -94,6 +94,7 @@ import { techDocsPage } from './components/techdocs/TechDocsPage'; import { ApacheAirflowPage } from '@backstage/plugin-apache-airflow'; import { PermissionedRoute } from '@backstage/plugin-permission-react'; import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; +import { TodoListCommonPage } from 'plugin-todo-list-common'; const app = createApp({ apis, @@ -233,6 +234,7 @@ const routes = ( } /> } /> + } /> );