Skip to content

Commit

Permalink
Merge pull request backstage#24518 from Zaperex/add-additional-scaffo…
Browse files Browse the repository at this point in the history
…lder-permissions

Add additional scaffolder permissions
  • Loading branch information
benjdlambert authored May 23, 2024
2 parents 5b79440 + a1218fc commit 5b95868
Show file tree
Hide file tree
Showing 26 changed files with 574 additions and 87 deletions.
11 changes: 11 additions & 0 deletions .changeset/tender-seas-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@backstage/plugin-scaffolder-react': patch
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-catalog': patch
---

updated the ContextMenu, ActionsPage, OngoingTask and TemplateCard frontend components to support the new scaffolder permissions:

- `scaffolder.task.create`
- `scaffolder.task.cancel`
- `scaffolder.task.read`
10 changes: 10 additions & 0 deletions .changeset/weak-gifts-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-scaffolder-common': patch
---

added the following new permissions to the scaffolder backend endpoints:

- `scaffolder.task.create`
- `scaffolder.task.cancel`
- `scaffolder.task.read`
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
id: authorizing-parameters-steps-and-actions
title: 'Authorizing parameters, steps and actions'
description: How to authorize part of a template
id: authorizing-scaffolder-template-details
title: 'Authorizing scaffolder tasks, parameters, steps, and actions'
description: How to authorize parts of a template and authorize scaffolder task access
---

The scaffolder plugin integrates with the Backstage [permission framework](../../permissions/overview.md), which allows you to control access to certain parameters and steps in your templates based on the user executing the template.
The scaffolder plugin integrates with the Backstage [permission framework](../../permissions/overview.md), which allows you to control access to certain parameters and steps in your templates based on the user executing the template. It also allows you to control access to scaffolder tasks.

### Authorizing parameters and steps

Expand Down Expand Up @@ -174,7 +174,64 @@ class ExamplePermissionPolicy implements PermissionPolicy {
}
```

Although the rules exported by the scaffolder are simple, combining them can help you achieve more complex cases.
### Authorizing scaffolder tasks

The scaffolder plugin also exposes permissions that can restrict access to tasks, task logs, task creation, and task cancellation. This can be useful if you want to control who has access to these areas of the scaffolder.

```ts title="packages/src/backend/plugins/permissions.ts"
/* highlight-add-start */
import {
taskCancelPermission,
taskCreatePermission,
taskReadPermission,
} from '@backstage/plugin-scaffolder-common/alpha';
/* highlight-add-end */
class ExamplePermissionPolicy implements PermissionPolicy {
async handle(
request: PolicyQuery,
user?: BackstageIdentityResponse,
): Promise<PolicyDecision> {
/* highlight-add-start */
if (isPermission(request.permission, taskCreatePermission)) {
if (user?.identity.userEntityRef === 'user:default/spiderman') {
return {
result: AuthorizeResult.ALLOW,
};
}
}
if (isPermission(request.permission, taskCancelPermission)) {
if (user?.identity.userEntityRef === 'user:default/spiderman') {
return {
result: AuthorizeResult.ALLOW,
};
}
}
if (isPermission(request.permission, taskReadPermission)) {
if (user?.identity.userEntityRef === 'user:default/spiderman') {
return {
result: AuthorizeResult.ALLOW,
};
}
}
/* highlight-add-end */
return {
result: AuthorizeResult.DENY,
};
}
}
```

In the provided example permission policy, we only grant the `spiderman` user permissions to perform/access the following actions/resources:

- Read all scaffolder tasks and their associated events/logs.
- Cancel any ongoing scaffolder tasks.
- Trigger software templates, which effectively creates new scaffolder tasks.

Any other user would be denied access to these actions/resources.

Although the rules exported by the scaffolder are simple, combining them can help you achieve more complex use cases.

### Authorizing in the New Backend System

Expand Down
4 changes: 4 additions & 0 deletions microsite/docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ const config: Config = {
from: '/docs/getting-started/configuration',
to: '/docs/getting-started/#next-steps',
},
{
from: '/docs/features/software-templates/authorizing-parameters-steps-and-actions',
to: '/docs/features/software-templates/authorizing-scaffolder-template-details',
},
],
},
],
Expand Down
2 changes: 1 addition & 1 deletion microsite/sidebars.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
"features/software-templates/writing-tests-for-actions",
"features/software-templates/writing-custom-field-extensions",
"features/software-templates/writing-custom-step-layouts",
"features/software-templates/authorizing-parameters-steps-and-actions",
"features/software-templates/authorizing-scaffolder-template-details",
"features/software-templates/migrating-to-rjsf-v5",
"features/software-templates/migrating-from-v1beta2-to-v1beta3"
]
Expand Down
3 changes: 2 additions & 1 deletion plugins/catalog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "^14.0.0",
"@types/pluralize": "^0.0.33"
"@types/pluralize": "^0.0.33",
"swr": "^2.2.5"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
Expand Down
61 changes: 58 additions & 3 deletions plugins/catalog/src/components/AboutCard/AboutCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { permissionApiRef } from '@backstage/plugin-permission-react';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import { SWRConfig } from 'swr';

const mockAuthorize = jest.fn();

Expand Down Expand Up @@ -546,7 +547,7 @@ describe('<AboutCard />', () => {
).not.toBeInTheDocument();
});

it('renders techdocs lin when 3rdparty', async () => {
it('renders techdocs link when 3rdparty', async () => {
const entity = {
apiVersion: 'v1',
kind: 'Component',
Expand Down Expand Up @@ -774,7 +775,9 @@ describe('<AboutCard />', () => {
namespace: 'default',
},
};

mockAuthorize.mockImplementation(async () => ({
result: AuthorizeResult.ALLOW,
}));
await renderInTestApp(
<TestApiProvider
apis={[
Expand All @@ -794,7 +797,7 @@ describe('<AboutCard />', () => {
),
],
[catalogApiRef, catalogApi],
[permissionApiRef, {}],
[permissionApiRef, mockPermissionApi],
]}
>
<EntityProvider entity={entity}>
Expand All @@ -816,6 +819,58 @@ describe('<AboutCard />', () => {
'/create/templates/default/create-react-app-template',
);
});
it('renders disabled launch template button if user has insufficient permissions', async () => {
const entity = {
apiVersion: 'scaffolder.backstage.io/v1beta3',
kind: 'Template',
metadata: {
name: 'create-react-app-template',
namespace: 'default',
},
};
mockAuthorize.mockImplementation(async () => ({
result: AuthorizeResult.DENY,
}));
await renderInTestApp(
<SWRConfig value={{ provider: () => new Map() }}>
<TestApiProvider
apis={[
[
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'github.com',
token: '...',
},
],
},
}),
),
],
[catalogApiRef, catalogApi],
[permissionApiRef, mockPermissionApi],
]}
>
<EntityProvider entity={entity}>
<AboutCard />
</EntityProvider>
</TestApiProvider>
</SWRConfig>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
'/create/templates/:namespace/:templateName':
createFromTemplateRouteRef,
},
},
);

expect(screen.getByText('Launch Template')).toBeVisible();
expect(screen.getByText('Launch Template').closest('a')).toBeNull();
});

it.each([
{
Expand Down
10 changes: 8 additions & 2 deletions plugins/catalog/src/components/AboutCard/AboutCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
CompoundEntityRef,
DEFAULT_NAMESPACE,
stringifyEntityRef,
parseEntityRef,
} from '@backstage/catalog-model';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
Expand Down Expand Up @@ -58,10 +59,11 @@ import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import DocsIcon from '@material-ui/icons/Description';
import EditIcon from '@material-ui/icons/Edit';
import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { parseEntityRef } from '@backstage/catalog-model';
import { useEntityPermission } from '@backstage/plugin-catalog-react/alpha';
import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common/alpha';
import { useSourceTemplateCompoundEntityRef } from './hooks';
import { taskCreatePermission } from '@backstage/plugin-scaffolder-common/alpha';
import { usePermission } from '@backstage/plugin-permission-react';

const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref';

Expand Down Expand Up @@ -115,6 +117,10 @@ export function AboutCard(props: AboutCardProps) {
catalogEntityRefreshPermission,
);

const { allowed: canCreateTemplateTask } = usePermission({
permission: taskCreatePermission,
});

const entitySourceLocation = getEntitySourceLocation(
entity,
scmIntegrationsApi,
Expand Down Expand Up @@ -172,7 +178,7 @@ export function AboutCard(props: AboutCardProps) {
const launchTemplate: IconLinkVerticalProps = {
label: 'Launch Template',
icon: <Icon />,
disabled: !templateRoute,
disabled: !templateRoute || !canCreateTemplateTask,
href:
templateRoute &&
templateRoute({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
SecureTemplateRenderer,
} from '../../lib/templating/SecureTemplater';
import {
TaskRecovery,
TaskSpec,
TaskSpecV1beta3,
TaskStep,
Expand All @@ -52,7 +53,6 @@ import {
} from '@backstage/plugin-permission-common';
import { scaffolderActionRules } from '../../service/rules';
import { actionExecutePermission } from '@backstage/plugin-scaffolder-common/alpha';
import { TaskRecovery } from '@backstage/plugin-scaffolder-common';
import { PermissionsService } from '@backstage/backend-plugin-api';
import { loggerToWinstonLogger } from '@backstage/backend-common';
import { BackstageLoggerTransport, WinstonLogger } from './logger';
Expand Down
10 changes: 10 additions & 0 deletions plugins/scaffolder-backend/src/service/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,11 @@ describe('createRouter', () => {
result: AuthorizeResult.ALLOW,
},
]);
jest.spyOn(permissionApi, 'authorize').mockImplementation(async () => [
{
result: AuthorizeResult.ALLOW,
},
]);
});

afterEach(() => {
Expand Down Expand Up @@ -741,6 +746,11 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
result: AuthorizeResult.ALLOW,
},
]);
jest.spyOn(permissionApi, 'authorize').mockImplementation(async () => [
{
result: AuthorizeResult.ALLOW,
},
]);
});

afterEach(() => {
Expand Down
Loading

0 comments on commit 5b95868

Please sign in to comment.