Merge pull request #29202 from 04kash/scaffolder-task-granular-permissions

Scaffolder task granular permissions
This commit is contained in:
Ben Lambert
2025-07-08 12:54:43 +02:00
committed by GitHub
28 changed files with 875 additions and 101 deletions
@@ -91,14 +91,22 @@ const ListTaskPageContent = (props: MyTaskPageProps) => {
if (error) {
return (
<>
<ErrorPanel error={error} />
<EmptyState
missing="info"
title={t('listTaskPage.content.emptyState.title')}
description={t('listTaskPage.content.emptyState.description')}
/>
</>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<OwnerListPicker
filter={ownerFilter}
onSelectOwner={id => setOwnerFilter(id)}
/>
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<ErrorPanel error={error} />
<EmptyState
missing="info"
title={t('listTaskPage.content.emptyState.title')}
description={t('listTaskPage.content.emptyState.description')}
/>
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
);
}
@@ -46,6 +46,7 @@ type ContextMenuProps = {
onStartOver?: () => void;
onToggleLogs?: (state: boolean) => void;
onToggleButtonBar?: (state: boolean) => void;
taskId?: string;
isCancelButtonDisabled: boolean;
onCancel: () => void;
};
@@ -67,6 +68,7 @@ export const ContextMenu = (props: ContextMenuProps) => {
onStartOver,
onToggleLogs,
onToggleButtonBar,
taskId,
} = props;
const { getPageTheme } = useTheme();
const pageTheme = getPageTheme({ themeId: 'website' });
@@ -76,6 +78,7 @@ export const ContextMenu = (props: ContextMenuProps) => {
const { allowed: canReadTask } = usePermission({
permission: taskReadPermission,
resourceRef: taskId,
});
const { allowed: canCreateTask } = usePermission({
@@ -161,20 +161,43 @@ describe('OngoingTask', () => {
await expect(rendered.findByText('Hide Logs')).resolves.toBeInTheDocument();
});
it('should have cancel and start over buttons be disabled without the proper permissions', async () => {
it('should have cancel button be disabled when user has read permission but lacks cancel permission', async () => {
const permissionApi = mockApis.permission({
authorize: AuthorizeResult.DENY,
authorize: request => {
if (request.permission.name === 'scaffolder.task.cancel') {
return AuthorizeResult.DENY;
}
return AuthorizeResult.ALLOW;
},
});
const rendered = await render(permissionApi);
const { getByTestId } = rendered;
expect(getByTestId('cancel-button')).toHaveClass('Mui-disabled');
expect(getByTestId('start-over-button')).toHaveClass('Mui-disabled');
await act(async () => {
fireEvent.click(getByTestId('menu-button'));
});
expect(getByTestId('cancel-task')).toHaveClass('Mui-disabled');
expect(getByTestId('start-over-task')).toHaveClass('Mui-disabled');
});
it('should have start over button be disabled when user has read permission but lacks create permission', async () => {
const permissionApi = mockApis.permission({
authorize: request => {
if (request.permission.name === 'scaffolder.task.create') {
return AuthorizeResult.DENY;
}
return AuthorizeResult.ALLOW;
},
});
const rendered = await render(permissionApi);
const { getByTestId } = rendered;
expect(getByTestId('start-over-button')).toHaveClass('Mui-disabled');
await act(async () => {
fireEvent.click(getByTestId('menu-button'));
});
expect(getByTestId('start-over-button')).toHaveClass('Mui-disabled');
});
});
@@ -139,13 +139,14 @@ function OngoingTaskContent(props: {
const [logsVisible, setLogVisibleState] = useState(false);
const [buttonBarVisible, setButtonBarVisibleState] = useState(true);
// Used dummy string value for `resourceRef` since `allowed` field will always return `false` if `resourceRef` is `undefined`
const { allowed: canCancelTask } = usePermission({
permission: taskCancelPermission,
resourceRef: taskId,
});
const { allowed: canReadTask } = usePermission({
permission: taskReadPermission,
resourceRef: taskId,
});
const { allowed: canCreateTask } = usePermission({
@@ -269,6 +270,7 @@ function OngoingTaskContent(props: {
onRetry={triggerRetry}
onToggleLogs={setLogVisibleState}
onToggleButtonBar={setButtonBarVisibleState}
taskId={taskId}
onCancel={triggerCancel}
isCancelButtonDisabled={isCancelButtonDisabled}
/>
@@ -60,10 +60,7 @@ import {
CustomFieldsPage,
} from '../../alpha/components/TemplateEditorPage';
import { RequirePermission } from '@backstage/plugin-permission-react';
import {
taskReadPermission,
templateManagementPermission,
} from '@backstage/plugin-scaffolder-common/alpha';
import { templateManagementPermission } from '@backstage/plugin-scaffolder-common/alpha';
import { useApp } from '@backstage/core-plugin-api';
import { FormField, OpaqueFormField } from '@internal/scaffolder';
import { useAsync, useMountEffect } from '@react-hookz/web';
@@ -182,11 +179,9 @@ export const InternalRouter = (
<Route
path={scaffolderTaskRouteRef.path}
element={
<RequirePermission permission={taskReadPermission}>
<TaskPageComponent
TemplateOutputsComponent={TemplateOutputsComponent}
/>
</RequirePermission>
<TaskPageComponent
TemplateOutputsComponent={TemplateOutputsComponent}
/>
}
/>
<Route
@@ -230,11 +225,7 @@ export const InternalRouter = (
/>
<Route
path={scaffolderListTaskRouteRef.path}
element={
<RequirePermission permission={taskReadPermission}>
<ListTasksPage contextMenu={props.contextMenu} />
</RequirePermission>
}
element={<ListTasksPage contextMenu={props.contextMenu} />}
/>
<Route
path={editorRouteRef.path}