Merge pull request #24518 from Zaperex/add-additional-scaffolder-permissions

Add additional scaffolder permissions
This commit is contained in:
Ben Lambert
2024-05-23 09:25:02 +02:00
committed by GitHub
26 changed files with 574 additions and 87 deletions
+4 -1
View File
@@ -54,6 +54,7 @@
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/plugin-permission-react": "workspace:^",
"@backstage/plugin-scaffolder-common": "workspace:^",
"@backstage/theme": "workspace:^",
"@backstage/types": "workspace:^",
@@ -87,13 +88,15 @@
"@backstage/core-app-api": "workspace:^",
"@backstage/plugin-catalog": "workspace:^",
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-permission-common": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "^14.0.0",
"@types/humanize-duration": "^3.18.1",
"@types/luxon": "^3.0.0"
"@types/luxon": "^3.0.0",
"swr": "^2.0.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
@@ -14,7 +14,14 @@
* limitations under the License.
*/
import { ComponentType, ElementType, FormEvent, ReactNode, Ref } from 'react';
import {
ComponentType,
ElementType,
FormEvent,
HTMLAttributes,
ReactNode,
Ref,
} from 'react';
import {
ErrorSchema,
FormContextType,
@@ -32,7 +39,6 @@ import {
Experimental_DefaultFormStateBehavior,
ErrorTransformer,
} from '@rjsf/utils';
import { HTMLAttributes } from 'react';
import Form, { IChangeEvent } from '@rjsf/core';
/**
@@ -19,6 +19,7 @@ import {
starredEntitiesApiRef,
} from '@backstage/plugin-catalog-react';
import {
MockPermissionApi,
MockStorageApi,
renderInTestApp,
TestApiProvider,
@@ -28,6 +29,9 @@ import React from 'react';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { RELATION_OWNED_BY } from '@backstage/catalog-model';
import { fireEvent } from '@testing-library/react';
import { permissionApiRef } from '@backstage/plugin-permission-react';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import { SWRConfig } from 'swr';
describe('TemplateCard', () => {
it('should render the card title', async () => {
@@ -50,6 +54,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -79,6 +84,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -110,6 +116,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -139,6 +146,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -174,6 +182,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -213,6 +222,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -257,6 +267,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} additionalLinks={[]} />
@@ -305,6 +316,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} additionalLinks={[]} />
@@ -347,6 +359,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -386,6 +399,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} onSelected={mockOnSelected} />
@@ -403,4 +417,44 @@ describe('TemplateCard', () => {
expect(mockOnSelected).toHaveBeenCalledWith(mockTemplate);
});
it('should not render the choose button when user has insufficient permissions', async () => {
const mockTemplate: TemplateEntityV1beta3 = {
apiVersion: 'scaffolder.backstage.io/v1beta3',
kind: 'Template',
metadata: { name: 'bob', tags: ['cpp', 'react'] },
spec: {
steps: [],
type: 'service',
},
};
const mockOnSelected = jest.fn();
const mockAuthorize = jest
.fn()
.mockImplementation(async () => ({ result: AuthorizeResult.DENY }));
// SWR used by the usePermission hook needs cache to be reset for each test
const { queryByText } = await renderInTestApp(
<SWRConfig value={{ provider: () => new Map() }}>
<TestApiProvider
apis={[
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi(mockAuthorize)],
]}
>
<TemplateCard template={mockTemplate} onSelected={mockOnSelected} />
</TestApiProvider>
</SWRConfig>,
{
mountedRoutes: {
'/catalog/:kind/:namespace/:name': entityRouteRef,
},
},
);
expect(queryByText('Choose')).toBeNull();
});
});
@@ -39,6 +39,8 @@ import LanguageIcon from '@material-ui/icons/Language';
import React, { useCallback } from 'react';
import { CardHeader } from './CardHeader';
import { CardLink } from './CardLink';
import { usePermission } from '@backstage/plugin-permission-react';
import { taskCreatePermission } from '@backstage/plugin-scaffolder-common/alpha';
const useStyles = makeStyles<Theme>(theme => ({
box: {
@@ -108,6 +110,9 @@ export const TemplateCard = (props: TemplateCardProps) => {
!!props.additionalLinks?.length || !!template.metadata.links?.length;
const displayDefaultDivider = !hasTags && !hasLinks;
const { allowed: canCreateTask } = usePermission({
permission: taskCreatePermission,
});
const handleChoose = useCallback(() => {
analytics.captureEvent('click', `Template has been opened`);
onSelected?.(template);
@@ -196,14 +201,16 @@ export const TemplateCard = (props: TemplateCardProps) => {
</>
)}
</div>
<Button
size="small"
variant="outlined"
color="primary"
onClick={handleChoose}
>
Choose
</Button>
{canCreateTask ? (
<Button
size="small"
variant="outlined"
color="primary"
onClick={handleChoose}
>
Choose
</Button>
) : null}
</div>
</CardActions>
</Card>