Merge branch 'backstage:master' into master

This commit is contained in:
Waldir Montoya
2023-02-06 19:39:30 -05:00
committed by GitHub
240 changed files with 4005 additions and 2203 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

+3 -1
View File
@@ -29,7 +29,9 @@ auth:
jwtHeader: x-custom-header # Optional: Only if you are using a custom header for the IAP JWT
```
You can find the project number and service ID in the Google Cloud Console.
The full `audience` value can be obtained by visiting your [Identity-Aware Proxy Google Cloud console](https://console.cloud.google.com/security/iap), selecting your project, finding your Backend Service to proxy, clicking the 3 vertical dots then "Get JWT Audience Code", and copying from the resulting popup, which will look similar to the following:
![Identity-Aware Proxy JWT Audience Code popup](../../assets/auth/gcp-iap-jwt-audience-code-popup.png)
This config section must be in place for the provider to load at all. Now let's
add the provider itself.
@@ -22,7 +22,7 @@ import {
} from '@backstage/backend-plugin-api';
export const examplePlugin = createBackendPlugin({
id: 'example',
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
@@ -53,7 +53,7 @@ export interface ExamplePluginOptions {
export const examplePlugin = createBackendPlugin(
(options?: ExamplePluginOptions) => ({
id: 'example',
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
@@ -44,7 +44,7 @@ class ActionsExtension implements ScaffolderActionsExtensionPoint {
export const scaffolderPlugin = createBackendPlugin(
{
id: 'scaffolder',
pluginId: 'scaffolder',
register(env) {
const actionsExtensions = new ActionsExtension();
env.registerExtensionPoint(
@@ -21,7 +21,7 @@ Example:
```ts
export const catalogPlugin = createBackendPlugin({
id: 'catalog',
pluginId: 'catalog',
...
})
```
@@ -36,7 +36,7 @@ export const catalogPlugin = createBackendPlugin({
Example:
```ts
export const catalogModuleGithubEntityProvider = createBackendPlugin({
export const catalogModuleGithubEntityProvider = createBackendModule({
pluginId: 'catalog',
moduleId: 'githubEntityProvider',
...
@@ -36,7 +36,7 @@ import {
import { createExampleRouter } from './router';
export const examplePlugin = createBackendPlugin({
id: 'example',
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
@@ -183,7 +183,7 @@ that needs to be different across environments.
import { coreServices } from '@backstage/backend-plugin-api';
export const examplePlugin = createBackendPlugin({
id: 'example',
pluginId: 'example',
register(env) {
env.registerInit({
deps: { config: coreServices.config },
@@ -216,7 +216,7 @@ export interface ExampleOptions {
export const examplePlugin = createBackendPlugin(
(options?: ExampleOptions) => ({
id: 'example',
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
@@ -46,7 +46,7 @@ import { Router } from 'express';
import { KubernetesBuilder } from './KubernetesBuilder';
export const kubernetesPlugin = createBackendPlugin({
id: 'kubernetes',
pluginId: 'kubernetes',
register(env) {
env.registerInit({
deps: {
@@ -95,7 +95,7 @@ export interface KubernetesOptions {
}
const kubernetesPlugin = createBackendPlugin((options: KubernetesOptions) => ({
id: 'kubernetes',
pluginId: 'kubernetes',
register(env) {
env.registerInit({
deps: {
@@ -182,7 +182,7 @@ class ClusterSupplier implements KubernetesClusterSupplierExtensionPoint {
}
export const kubernetesPlugin = createBackendPlugin({
id: 'kubernetes',
pluginId: 'kubernetes',
register(env) {
const extensionPoint = new ClusterSupplier();
// We register the extension point with the backend, which allows modules to
+26 -27
View File
@@ -33,7 +33,7 @@ import {
import { Router } from 'express';
createBackendPlugin({
id: 'example',
pluginId: 'example',
register(env) {
env.registerInit({
deps: { http: coreServices.httpRouter },
@@ -86,7 +86,7 @@ import {
import { Router } from 'express';
createBackendPlugin({
id: 'example',
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
@@ -157,7 +157,7 @@ import {
} from '@backstage/backend-plugin-api';
createBackendPlugin({
id: 'example',
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
@@ -215,7 +215,7 @@ import {
} from '@backstage/backend-plugin-api';
createBackendPlugin({
id: 'example',
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
@@ -288,7 +288,7 @@ import {
} from '@backstage/backend-plugin-api';
createBackendPlugin({
id: 'example',
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
@@ -327,7 +327,7 @@ import {
import { resolvePackagePath } from '@backstage/backend-common';
createBackendPlugin({
id: 'example',
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
@@ -366,7 +366,7 @@ import {
import { fetch } from 'node-fetch';
createBackendPlugin({
id: 'example',
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
@@ -397,7 +397,7 @@ import {
import { Router } from 'express';
createBackendPlugin({
id: 'example',
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
@@ -465,7 +465,7 @@ import {
} from '@backstage/backend-plugin-api';
createBackendPlugin({
id: 'example',
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
@@ -479,10 +479,7 @@ createBackendPlugin({
// do some other stuff.
}, 1000);
lifecycle.addShutdownHook({
fn: () => clearInterval(interval),
logger,
});
lifecycle.addShutdownHook(() => clearInterval(interval));
},
});
},
@@ -499,17 +496,19 @@ The following example shows how to override the default implementation of the li
```ts
class MyCustomLifecycleService implements RootLifecycleService {
constructor(private readonly logger: LoggerService) {
['SIGKILL', 'SIGTERM'].map(signal =>
process.on(signal, () => this.shutdown()),
);
}
constructor(private readonly logger: LoggerService) {}
#isCalled = false;
#shutdownTasks: Array<LifecycleServiceShutdownHook> = [];
#shutdownTasks: Array<{
hook: LifecycleServiceShutdownHook;
options?: LifecycleServiceShutdownOptions;
}> = [];
addShutdownHook(options: LifecycleServiceShutdownHook): void {
this.#shutdownTasks.push(options);
addShutdownHook(
hook: LifecycleServiceShutdownHook,
options?: LifecycleServiceShutdownOptions,
): void {
this.#shutdownTasks.push({ hook, options });
}
async shutdown(): Promise<void> {
@@ -520,10 +519,10 @@ class MyCustomLifecycleService implements RootLifecycleService {
this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`);
await Promise.all(
this.#shutdownTasks.map(async hook => {
const { logger = this.logger } = hook;
this.#shutdownTasks.map(async ({ hook, options }) => {
const logger = options?.logger ?? this.logger;
try {
await hook.fn();
await hook();
logger.info(`Shutdown hook succeeded`);
} catch (error) {
logger.error(`Shutdown hook failed, ${error}`);
@@ -564,7 +563,7 @@ import {
import { Router } from 'express';
createBackendPlugin({
id: 'example',
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
@@ -613,7 +612,7 @@ import {
import { fetch } from 'node-fetch';
createBackendPlugin({
id: 'example',
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
@@ -652,7 +651,7 @@ import {
import os from 'os';
createBackendPlugin({
id: 'example',
pluginId: 'example',
register(env) {
env.registerInit({
deps: {
@@ -112,11 +112,11 @@ steps which would be rendered as different steps in the scaffolder plugin
frontend.
Each `Step` is `JSONSchema` with some extra goodies for styling what it might
look like in the frontend. For these steps we rely very heavily on this library:
https://github.com/rjsf-team/react-jsonschema-form. They have some great docs
too here: https://react-jsonschema-form.readthedocs.io/ and a playground where
you can play around with some examples here
https://rjsf-team.github.io/react-jsonschema-form.
look like in the frontend. For these steps we rely very heavily on this
[library](https://github.com/rjsf-team/react-jsonschema-form). They have some
[great docs](https://rjsf-team.github.io/react-jsonschema-form/docs/) and a
[playground](https://rjsf-team.github.io/react-jsonschema-form) where you can
play around with some examples.
There's another option for that library called `uiSchema` which we've taken
advantage of, and we've merged it with the existing `JSONSchema` that you
+2
View File
@@ -28,3 +28,5 @@ The configuration is a structure with two elements:
- `host`: The DevOps host; only `dev.azure.com` is supported.
- `token` (optional): A personal access token as expected by Azure DevOps.
> Note: The token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API
+1 -1
View File
@@ -22,7 +22,7 @@ catalog:
yourProviderId:
host: gitlab-host # Identifies one of the hosts set up in the integrations
branch: main # Optional. Uses `master` as default
group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole project will be scanned
group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole instance will be scanned
entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml`
projectPattern: /[\s\S]*/ # Optional. Filters found projects based on provided patter. Defaults to `/[\s\S]*/`, what means to not filter anything
schedule: # optional; same options as in TaskScheduleDefinition
+1 -1
View File
@@ -48,7 +48,7 @@ The source code is available here:
Create a new `packages/backend/src/plugins/todolist.ts` with the following content:
```javascript
```typescript
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
import { createRouter } from '@internal/plugin-todo-list-backend';
import { Router } from 'express';
@@ -146,7 +146,7 @@ In order to test the logic above, the integrators of your backstage instance nee
- async handle(): Promise<PolicyDecision> {
+ async handle(
+ request: PolicyQuery,
+ user?: BackstageIdentityResponse,
+ _user?: BackstageIdentityResponse,
+ ): Promise<PolicyDecision> {
+ if (isPermission(request.permission, todoListCreatePermission)) {
+ return {
@@ -204,7 +204,7 @@ First we'll clean up the `plugins/todo-list-backend/src/service/router.test.ts`:
const router = await createRouter({
logger: getVoidLogger(),
identity: {} as DefaultIdentityClient,
+ permissions: toPermissionEvaluator,
+ permissions: permissionEvaluator,
});
app = express().use(router);
});
@@ -235,7 +235,7 @@ Then we want to update the `plugins/todo-list-backend/src/service/standaloneServ
+ ServerTokenManager,
} from '@backstage/backend-common';
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
+ import { ServerPermissionClient } from '@backstage/plugin-permission-node';
import { Server } from 'http';
import { Logger } from 'winston';
import { createRouter } from './router';
@@ -285,4 +285,44 @@ Then we want to update the `plugins/todo-list-backend/src/service/standaloneServ
module.hot?.accept();
```
Finally, we need to update `plugins/todo-list-backend/src/plugin.ts`:
```diff
import { loggerToWinstonLogger } from '@backstage/backend-common';
import {
coreServices,
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import { createRouter } from './service/router';
/**
* The example TODO list backend plugin.
*
* @alpha
*/
export const exampleTodoListPlugin = createBackendPlugin({
pluginId: 'exampleTodoList',
register(env) {
env.registerInit({
deps: {
identity: coreServices.identity,
logger: coreServices.logger,
httpRouter: coreServices.httpRouter,
+ permissions: coreServices.permissions,
},
- async init({ identity, logger, httpRouter }) {
+ async init({ identity, logger, httpRouter, permissions }) {
httpRouter.use(
await createRouter({
identity,
logger: loggerToWinstonLogger(logger),
permissions,
}),
);
},
});
},
});
```
Now when you run `yarn tsc` you should have no more errors.
@@ -76,7 +76,7 @@ This enables decisions based on characteristics of the resource, but it's import
Install the missing module:
```
$ yarn workspace @internal/plugin-todo-list-backend add @backstage/plugin-permission-node
$ yarn workspace @internal/plugin-todo-list-backend add @backstage/plugin-permission-node zod
```
Create a new `plugins/todo-list-backend/src/service/rules.ts` file and append the following code:
@@ -89,7 +89,8 @@ import { Todo, TodoFilter } from './todos';
export const createTodoListPermissionRule = makeCreatePermissionRule<
Todo,
TodoFilter
TodoFilter,
typeof TODO_LIST_RESOURCE_TYPE
>();
export const isOwner = createTodoListPermissionRule({
@@ -97,8 +98,8 @@ export const isOwner = createTodoListPermissionRule({
description: 'Should allow only if the todo belongs to the user',
resourceType: TODO_LIST_RESOURCE_TYPE,
paramsSchema: z.object({
userId: z.string().describe('User ID to match on the resource')
})
userId: z.string().describe('User ID to match on the resource'),
}),
apply: (resource: Todo, { userId }) => {
return resource.author === userId;
},
@@ -187,6 +188,7 @@ Make sure `todoListConditions` and `createTodoListConditionalDecision` are expor
```diff
export * from './service/router';
+ export * from './conditionExports';
export { exampleTodoListPlugin } from './plugin';
```
## Test the authorized update endpoint
@@ -209,7 +211,6 @@ Let's go back to the permission policy's handle function and try to authorize ou
+ import {
+ todoListCreatePermission,
+ todoListUpdatePermission,
+ TODO_LIST_RESOURCE_TYPE,
+ } from '@internal/plugin-todo-list-common';
+ import {
+ todoListConditions,
@@ -217,7 +218,11 @@ Let's go back to the permission policy's handle function and try to authorize ou
+ } from '@internal/plugin-todo-list-backend';
...
async handle(
request: PolicyQuery,
- _user?: BackstageIdentityResponse,
+ user?: BackstageIdentityResponse,
): Promise<PolicyDecision> {
if (isPermission(request.permission, todoListCreatePermission)) {
return {
result: AuthorizeResult.ALLOW,
@@ -236,6 +241,7 @@ Let's go back to the permission policy's handle function and try to authorize ou
return {
result: AuthorizeResult.ALLOW,
};
}
```
For any incoming update requests, we now return a _Conditional Decision_. We are saying:
@@ -15,7 +15,7 @@ One possible solution may leverage the batching functionality to authorize all o
- res.json(getAll())
+ const items = getAll();
+ const decisions = await permissions.authorize(
+ items.map(({ id }) => ({ permission: todosListRead, resourceRef: id })),
+ items.map(({ id }) => ({ permission: todoListReadPermission, resourceRef: id })),
+ );
+ const filteredItems = decisions.filter(
@@ -53,7 +53,7 @@ Let's add another permission to the plugin.
resourceType: TODO_LIST_RESOURCE_TYPE,
});
+
+ export const todosListRead = createPermission({
+ export const todoListReadPermission = createPermission({
+ name: 'todos.list.read',
+ attributes: { action: 'read' },
+ resourceType: TODO_LIST_RESOURCE_TYPE,
@@ -81,10 +81,9 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev
import {
todosListCreate,
todosListUpdate,
+ todosListRead,
+ todoListReadPermission,
TODO_LIST_RESOURCE_TYPE,
} from './permissions';
+ import { rules } from './rules';
+ const transformConditions: ConditionTransformer<TodoFilter> = createConditionTransformer(Object.values(rules));
@@ -95,7 +94,7 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev
+ );
+
+ const decision = (
+ await permissions.authorizeConditional([{ permission: todosListRead }], {
+ await permissions.authorizeConditional([{ permission: todoListReadPermission }], {
+ token,
+ })
+ )[0];
@@ -110,7 +109,6 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev
+ } else {
+ res.json(getAll());
+ }
+ }
- res.json(getAll());
});
```
@@ -121,7 +119,7 @@ Since `TodoFilter` used in our plugin matches the structure of the conditions ob
## Test the authorized read endpoint
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.
Let's update our permission policy to return a conditional result whenever a `todoListReadPermission` permission is received. In this case, we can reuse the decision returned for the `todosListCreate` permission.
```diff
// packages/backend/src/plugins/permission.ts
@@ -132,7 +130,6 @@ import {
todoListCreatePermission,
todoListUpdatePermission,
+ todoListReadPermission,
TODO_LIST_RESOURCE_TYPE,
} from '@internal/plugin-todo-list-common';
...
@@ -56,7 +56,11 @@ Let's make the following changes in `plugins/todo-list/src/components/TodoListPa
- Add
- </Button>
+ {!loadingPermission && (
+ <Button disabled={!canAddTodo} variant="contained" onClick={handleAdd}>
+ <Button
+ disabled={!canAddTodo}
+ variant="contained"
+ onClick={() => onAdd(title.current)}
+ >
+ Add
+ </Button>
+ )}
@@ -118,7 +122,10 @@ Providing a disabled state can be a helpful signal to users, but there may be ca
- <Grid item>
- <AddTodo onAdd={handleAdd} />
- </Grid>
+ <RequirePermission permission={todoListCreatePermission} errorPage={<></>}>
+ <RequirePermission
+ permission={todoListCreatePermission}
+ errorPage={<></>}
+ >
+ <Grid item>
+ <AddTodo onAdd={handleAdd} />
+ </Grid>
@@ -149,11 +156,15 @@ Providing a disabled state can be a helpful signal to users, but there may be ca
onChange={e => (title.current = e.target.value)}
/>
- {!loadingPermission && (
- <Button disabled={!canAddTodo} variant="contained" onClick={handleAdd}>
- <Button
- disabled={!canAddTodo}
- variant="contained"
- onClick={() => onAdd(title.current)}
- >
- Add
- </Button>
- )}
+ <Button variant="contained" onClick={handleAdd}>
+ <Button variant="contained" onClick={() => onAdd(title.current)}>
+ Add
+ </Button>
</Box>
+2 -2
View File
@@ -84,7 +84,7 @@ import {
// export type ExamplePluginOptions = { exampleOption: boolean };
export const examplePlugin = createBackendPlugin({
// unique id for the plugin
id: 'example',
pluginId: 'example',
// It's possible to provide options to the plugin
// register(env, options: ExamplePluginOptions) {
register(env) {
@@ -111,7 +111,7 @@ If we wanted our plugin to accept options as well, we'd accept the options as th
```ts
export const examplePlugin = createBackendPlugin({
id: 'example',
pluginId: 'example',
register(env, options?: { silent?: boolean }) {
env.registerInit({
deps: { logger: coreServices.logger },