@@ -46,67 +46,67 @@ The permissions framework uses a new `permission-backend` plugin to accept autho
|
||||
|
||||
1. Add `@backstage/plugin-permission-backend` as a dependency of your Backstage backend:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
$ yarn add --cwd packages/backend @backstage/plugin-permission-backend
|
||||
```
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
$ yarn add --cwd packages/backend @backstage/plugin-permission-backend
|
||||
```
|
||||
|
||||
2. Add the following to a new file, `packages/backend/src/plugins/permission.ts`. This adds the permission-backend router, and configures it with a policy which allows everything.
|
||||
|
||||
```typescript title="packages/backend/src/plugins/permission.ts"
|
||||
import { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
```typescript title="packages/backend/src/plugins/permission.ts"
|
||||
import { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
async handle(): Promise<PolicyDecision> {
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
async handle(): Promise<PolicyDecision> {
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter({
|
||||
config: env.config,
|
||||
logger: env.logger,
|
||||
discovery: env.discovery,
|
||||
policy: new TestPermissionPolicy(),
|
||||
identity: env.identity,
|
||||
});
|
||||
}
|
||||
```
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter({
|
||||
config: env.config,
|
||||
logger: env.logger,
|
||||
discovery: env.discovery,
|
||||
policy: new TestPermissionPolicy(),
|
||||
identity: env.identity,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
3. Wire up the permission policy in `packages/backend/src/index.ts`. [The index in the example backend](https://github.com/backstage/backstage/blob/master/packages/backend/src/index.ts) shows how to do this. You’ll need to import the module from the previous step, create a plugin environment, and add the router to the express app:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import proxy from './plugins/proxy';
|
||||
import techdocs from './plugins/techdocs';
|
||||
import search from './plugins/search';
|
||||
/* highlight-add-next-line */
|
||||
import permission from './plugins/permission';
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import proxy from './plugins/proxy';
|
||||
import techdocs from './plugins/techdocs';
|
||||
import search from './plugins/search';
|
||||
/* highlight-add-next-line */
|
||||
import permission from './plugins/permission';
|
||||
|
||||
async function main() {
|
||||
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
|
||||
const searchEnv = useHotMemoize(module, () => createEnv('search'));
|
||||
const appEnv = useHotMemoize(module, () => createEnv('app'));
|
||||
/* highlight-add-next-line */
|
||||
const permissionEnv = useHotMemoize(module, () => createEnv('permission'));
|
||||
// ..
|
||||
async function main() {
|
||||
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
|
||||
const searchEnv = useHotMemoize(module, () => createEnv('search'));
|
||||
const appEnv = useHotMemoize(module, () => createEnv('app'));
|
||||
/* highlight-add-next-line */
|
||||
const permissionEnv = useHotMemoize(module, () => createEnv('permission'));
|
||||
// ..
|
||||
|
||||
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
|
||||
apiRouter.use('/proxy', await proxy(proxyEnv));
|
||||
apiRouter.use('/search', await search(searchEnv));
|
||||
/* highlight-add-next-line */
|
||||
apiRouter.use('/permission', await permission(permissionEnv));
|
||||
// ..
|
||||
}
|
||||
```
|
||||
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
|
||||
apiRouter.use('/proxy', await proxy(proxyEnv));
|
||||
apiRouter.use('/search', await search(searchEnv));
|
||||
/* highlight-add-next-line */
|
||||
apiRouter.use('/permission', await permission(permissionEnv));
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Enable and test the permissions system
|
||||
|
||||
@@ -114,46 +114,46 @@ Now that the permission backend is running, it’s time to enable the permission
|
||||
|
||||
1. Set the property `permission.enabled` to `true` in `app-config.yaml`.
|
||||
|
||||
```yaml title="app-config.yaml"
|
||||
permission:
|
||||
enabled: true
|
||||
```
|
||||
```yaml title="app-config.yaml"
|
||||
permission:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
2. Update the PermissionPolicy in `packages/backend/src/plugins/permission.ts` to disable a permission that’s easy for us to test. This policy rejects any attempt to delete a catalog entity:
|
||||
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
import { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
/* highlight-remove-next-line */
|
||||
import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
PermissionPolicy,
|
||||
PolicyQuery,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-end */
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
import { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
/* highlight-remove-next-line */
|
||||
import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
PermissionPolicy,
|
||||
PolicyQuery,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-end */
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
/* highlight-remove-next-line */
|
||||
async handle(): Promise<PolicyDecision> {
|
||||
/* highlight-add-start */
|
||||
async handle(request: PolicyQuery): Promise<PolicyDecision> {
|
||||
if (request.permission.name === 'catalog.entity.delete') {
|
||||
return {
|
||||
result: AuthorizeResult.DENY,
|
||||
};
|
||||
}
|
||||
/* highlight-add-end */
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
/* highlight-remove-next-line */
|
||||
async handle(): Promise<PolicyDecision> {
|
||||
/* highlight-add-start */
|
||||
async handle(request: PolicyQuery): Promise<PolicyDecision> {
|
||||
if (request.permission.name === 'catalog.entity.delete') {
|
||||
return {
|
||||
result: AuthorizeResult.DENY,
|
||||
};
|
||||
}
|
||||
/* highlight-add-end */
|
||||
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
```
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Now that you’ve made this change, you should find that the unregister entity menu option on the catalog entity page is disabled.
|
||||
|
||||
|
||||
@@ -20,99 +20,99 @@ The source code is available here:
|
||||
|
||||
1. Copy-paste the three folders into the plugins folder of your backstage application repository (removing the `example-` prefix from each folder) or run the following script from the root of your backstage application:
|
||||
|
||||
```bash
|
||||
$ cd $(mktemp -d)
|
||||
git clone --depth 1 --quiet --no-checkout --filter=blob:none https://github.com/backstage/backstage.git .
|
||||
git checkout master -- plugins/example-todo-list/
|
||||
git checkout master -- plugins/example-todo-list-backend/
|
||||
git checkout master -- plugins/example-todo-list-common/
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list/package.json
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-backend/package.json
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-common/package.json
|
||||
for file in plugins/*; do mv "$file" "$OLDPWD/${file/example-todo/todo}"; done
|
||||
cd -
|
||||
```
|
||||
```bash
|
||||
$ cd $(mktemp -d)
|
||||
git clone --depth 1 --quiet --no-checkout --filter=blob:none https://github.com/backstage/backstage.git .
|
||||
git checkout master -- plugins/example-todo-list/
|
||||
git checkout master -- plugins/example-todo-list-backend/
|
||||
git checkout master -- plugins/example-todo-list-common/
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list/package.json
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-backend/package.json
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-common/package.json
|
||||
for file in plugins/*; do mv "$file" "$OLDPWD/${file/example-todo/todo}"; done
|
||||
cd -
|
||||
```
|
||||
|
||||
The `plugins` directory of your project should now include `todo-list`, `todo-list-backend`, and `todo-list-common`.
|
||||
The `plugins` directory of your project should now include `todo-list`, `todo-list-backend`, and `todo-list-common`.
|
||||
|
||||
**Important**: if you are on **Windows**, make sure you have WSL and git installed on your machine before executing the script above.
|
||||
**Important**: if you are on **Windows**, make sure you have WSL and git installed on your machine before executing the script above.
|
||||
|
||||
2. Add these packages as dependencies for your Backstage app:
|
||||
|
||||
```sh
|
||||
# From your Backstage root directory
|
||||
$ yarn add --cwd packages/backend @internal/plugin-todo-list-backend @internal/plugin-todo-list-common
|
||||
$ yarn add --cwd packages/app @internal/plugin-todo-list
|
||||
```
|
||||
```sh
|
||||
# From your Backstage root directory
|
||||
$ yarn add --cwd packages/backend @internal/plugin-todo-list-backend @internal/plugin-todo-list-common
|
||||
$ yarn add --cwd packages/app @internal/plugin-todo-list
|
||||
```
|
||||
|
||||
3. Include the backend and frontend plugin in your application:
|
||||
|
||||
Create a new `packages/backend/src/plugins/todolist.ts` with the following content:
|
||||
Create a new `packages/backend/src/plugins/todolist.ts` with the following content:
|
||||
|
||||
```typescript title="packages/backend/src/plugins/todolist.ts"
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { createRouter } from '@internal/plugin-todo-list-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
```typescript title="packages/backend/src/plugins/todolist.ts"
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { createRouter } from '@internal/plugin-todo-list-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
discovery,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
return await createRouter({
|
||||
logger,
|
||||
identity: DefaultIdentityClient.create({
|
||||
discovery,
|
||||
issuer: await discovery.getExternalBaseUrl('auth'),
|
||||
}),
|
||||
});
|
||||
}
|
||||
```
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
discovery,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
return await createRouter({
|
||||
logger,
|
||||
identity: DefaultIdentityClient.create({
|
||||
discovery,
|
||||
issuer: await discovery.getExternalBaseUrl('auth'),
|
||||
}),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Apply the following changes to `packages/backend/src/index.ts`:
|
||||
Apply the following changes to `packages/backend/src/index.ts`:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import techdocs from './plugins/techdocs';
|
||||
/* highlight-add-next-line */
|
||||
import todoList from './plugins/todolist';
|
||||
import search from './plugins/search';
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import techdocs from './plugins/techdocs';
|
||||
/* highlight-add-next-line */
|
||||
import todoList from './plugins/todolist';
|
||||
import search from './plugins/search';
|
||||
|
||||
async function main() {
|
||||
const searchEnv = useHotMemoize(module, () => createEnv('search'));
|
||||
const appEnv = useHotMemoize(module, () => createEnv('app'));
|
||||
/* highlight-add-next-line */
|
||||
const todoListEnv = useHotMemoize(module, () => createEnv('todolist'));
|
||||
// ..
|
||||
async function main() {
|
||||
const searchEnv = useHotMemoize(module, () => createEnv('search'));
|
||||
const appEnv = useHotMemoize(module, () => createEnv('app'));
|
||||
/* highlight-add-next-line */
|
||||
const todoListEnv = useHotMemoize(module, () => createEnv('todolist'));
|
||||
// ..
|
||||
|
||||
apiRouter.use('/proxy', await proxy(proxyEnv));
|
||||
apiRouter.use('/search', await search(searchEnv));
|
||||
apiRouter.use('/permission', await permission(permissionEnv));
|
||||
/* highlight-add-next-line */
|
||||
apiRouter.use('/todolist', await todoList(todoListEnv));
|
||||
// Add backends ABOVE this line; this 404 handler is the catch-all fallback
|
||||
apiRouter.use(notFoundHandler());
|
||||
// ..
|
||||
}
|
||||
```
|
||||
apiRouter.use('/proxy', await proxy(proxyEnv));
|
||||
apiRouter.use('/search', await search(searchEnv));
|
||||
apiRouter.use('/permission', await permission(permissionEnv));
|
||||
/* highlight-add-next-line */
|
||||
apiRouter.use('/todolist', await todoList(todoListEnv));
|
||||
// Add backends ABOVE this line; this 404 handler is the catch-all fallback
|
||||
apiRouter.use(notFoundHandler());
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
Apply the following changes to `packages/app/src/App.tsx`:
|
||||
Apply the following changes to `packages/app/src/App.tsx`:
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-next-line */
|
||||
import { TodoListPage } from '@internal/plugin-todo-list';
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-next-line */
|
||||
import { TodoListPage } from '@internal/plugin-todo-list';
|
||||
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
<Route path="/search" element={<SearchPage />}>
|
||||
{searchPage}
|
||||
</Route>
|
||||
<Route path="/settings" element={<UserSettingsPage />} />
|
||||
{/* highlight-add-next-line */}
|
||||
<Route path="/todo-list" element={<TodoListPage />} />
|
||||
{/* ... */}
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
<Route path="/search" element={<SearchPage />}>
|
||||
{searchPage}
|
||||
</Route>
|
||||
<Route path="/settings" element={<UserSettingsPage />} />
|
||||
{/* highlight-add-next-line */}
|
||||
<Route path="/todo-list" element={<TodoListPage />} />
|
||||
{/* ... */}
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
|
||||
Now if you start your application you should be able to reach the `/todo-list` page:
|
||||
|
||||
|
||||
@@ -32,7 +32,10 @@ export const todoListUpdatePermission = createPermission({
|
||||
/* highlight-remove-next-line */
|
||||
export const todoListPermissions = [todoListCreatePermission];
|
||||
/* highlight-add-next-line */
|
||||
export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission];
|
||||
export const todoListPermissions = [
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
];
|
||||
```
|
||||
|
||||
Notice that unlike `todoListCreatePermission`, the `todoListUpdatePermission` 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.
|
||||
@@ -45,7 +48,10 @@ To start, let's edit `plugins/todo-list-backend/src/service/router.ts` in the sa
|
||||
/* highlight-remove-next-line */
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
/* highlight-add-next-line */
|
||||
import { todoListCreatePermission, todoListUpdatePermission } from '@internal/plugin-todo-list-common';
|
||||
import {
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
} from '@internal/plugin-todo-list-common';
|
||||
|
||||
router.put('/todos', async (req, res) => {
|
||||
/* highlight-add-start */
|
||||
|
||||
@@ -14,11 +14,14 @@ router.get('/todos', async (req, res) => {
|
||||
const token = IdentityClient.getBearerToken(req.header('authorization'));
|
||||
|
||||
/* highlight-remove-next-line */
|
||||
res.json(getAll())
|
||||
res.json(getAll());
|
||||
/* highlight-add-start */
|
||||
const items = getAll();
|
||||
const decisions = await permissions.authorize(
|
||||
items.map(({ id }) => ({ permission: todoListReadPermission, resourceRef: id })),
|
||||
items.map(({ id }) => ({
|
||||
permission: todoListReadPermission,
|
||||
resourceRef: id,
|
||||
})),
|
||||
);
|
||||
|
||||
const filteredItems = decisions.filter(
|
||||
@@ -64,9 +67,16 @@ export const todoListReadPermission = createPermission({
|
||||
/* highlight-add-end */
|
||||
|
||||
/* highlight-add-next-line */
|
||||
export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission];
|
||||
export const todoListPermissions = [
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
];
|
||||
/* highlight-add-next-line */
|
||||
export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission, todoListReadPermission];
|
||||
export const todoListPermissions = [
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
todoListReadPermission,
|
||||
];
|
||||
```
|
||||
|
||||
## Using conditional policy decisions
|
||||
|
||||
@@ -36,7 +36,9 @@ import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
function AddTodo({ onAdd }: { onAdd: (title: string) => any }) {
|
||||
const title = useRef('');
|
||||
/* highlight-add-next-line */
|
||||
const { loading: loadingPermission, allowed: canAddTodo } = usePermission({ permission: todoListCreatePermission });
|
||||
const { loading: loadingPermission, allowed: canAddTodo } = usePermission({
|
||||
permission: todoListCreatePermission,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -78,7 +80,6 @@ Here we are using the [`usePermission` hook](https://backstage.io/docs/reference
|
||||
It's really that simple! Let's change our policy to test the disabled button:
|
||||
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
/* highlight-remove-next-line */
|
||||
@@ -117,10 +118,7 @@ export const TodoListPage = () => {
|
||||
</Grid>
|
||||
{/* highlight-remove-end */}
|
||||
{/* highlight-add-start */}
|
||||
<RequirePermission
|
||||
permission={todoListCreatePermission}
|
||||
errorPage={<></>}
|
||||
>
|
||||
<RequirePermission permission={todoListCreatePermission} errorPage={<></>}>
|
||||
<Grid item>
|
||||
<AddTodo onAdd={handleAdd} />
|
||||
</Grid>
|
||||
@@ -129,13 +127,15 @@ export const TodoListPage = () => {
|
||||
<Grid item>
|
||||
<TodoList key={key} onEdit={setEdit} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
</Grid>;
|
||||
};
|
||||
|
||||
function AddTodo({ onAdd }: { onAdd: (title: string) => any }) {
|
||||
const title = useRef('');
|
||||
/* highlight-remove-next-line */
|
||||
const { loading: loadingPermission, allowed: canAddTodo } = usePermission({ permission: todoListCreatePermission });
|
||||
const { loading: loadingPermission, allowed: canAddTodo } = usePermission({
|
||||
permission: todoListCreatePermission,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user