diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 228f7bed12..3c98adc3f4 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -103,6 +103,7 @@ import { IdentityApiGetIdentityRequest, } from '@backstage/plugin-auth-node'; import { InternalTaskSecrets } from '../scaffolder/tasks/types'; +import { checkPermission } from '../util/checkPermissions'; /** * @@ -492,19 +493,11 @@ export async function createRouter( ) .get('/v2/actions', async (req, res) => { const credentials = await httpAuth.credentials(req); - - if (permissions) { - const authorizationResponse = ( - await permissions.authorizeConditional( - [{ permission: actionReadPermission }], - { credentials: credentials }, - ) - )[0]; - - if (authorizationResponse.result === AuthorizeResult.DENY) { - throw new NotAllowedError(); - } - } + await checkPermission({ + credentials, + permissions: [actionReadPermission], + permissionService: permissions, + }); const actionsList = actionRegistry.list().map(action => { return { id: action.id, @@ -522,18 +515,11 @@ export async function createRouter( }); const credentials = await httpAuth.credentials(req); - if (permissions) { - const authorizationResponse = ( - await permissions.authorizeConditional( - [{ permission: taskCreatePermission }], - { credentials: credentials }, - ) - )[0]; - - if (authorizationResponse.result === AuthorizeResult.DENY) { - throw new NotAllowedError(); - } - } + await checkPermission({ + credentials, + permissions: [taskCreatePermission], + permissionService: permissions, + }); const { token } = await auth.getPluginRequestToken({ onBehalfOf: credentials, @@ -612,22 +598,13 @@ export async function createRouter( }) .get('/v2/tasks', async (req, res) => { const credentials = await httpAuth.credentials(req); - - if (permissions) { - const authorizationResponse = ( - await permissions.authorizeConditional( - [{ permission: taskReadPermission }], - { credentials: credentials }, - ) - )[0]; - - if (authorizationResponse.result === AuthorizeResult.DENY) { - throw new NotAllowedError(); - } - } + await checkPermission({ + credentials, + permissions: [taskReadPermission], + permissionService: permissions, + }); const [userEntityRef] = [req.query.createdBy].flat(); - if ( typeof userEntityRef !== 'string' && typeof userEntityRef !== 'undefined' @@ -649,19 +626,11 @@ export async function createRouter( }) .get('/v2/tasks/:taskId', async (req, res) => { const credentials = await httpAuth.credentials(req); - - if (permissions) { - const authorizationResponse = ( - await permissions.authorizeConditional( - [{ permission: taskReadPermission }], - { credentials: credentials }, - ) - )[0]; - - if (authorizationResponse.result === AuthorizeResult.DENY) { - throw new NotAllowedError(); - } - } + await checkPermission({ + credentials, + permissions: [taskReadPermission], + permissionService: permissions, + }); const { taskId } = req.params; const task = await taskBroker.get(taskId); @@ -674,22 +643,12 @@ export async function createRouter( }) .post('/v2/tasks/:taskId/cancel', async (req, res) => { const credentials = await httpAuth.credentials(req); - - if (permissions) { - const authorizationResponses = await permissions.authorizeConditional( - [ - { permission: taskCancelPermission }, - { permission: taskReadPermission }, - ], - { credentials: credentials }, - ); - // Requires both read and cancel permissions - for (const response of authorizationResponses) { - if (response.result === AuthorizeResult.DENY) { - throw new NotAllowedError(); - } - } - } + // Requires both read and cancel permissions + await checkPermission({ + credentials, + permissions: [taskCancelPermission, taskReadPermission], + permissionService: permissions, + }); const { taskId } = req.params; await taskBroker.cancel?.(taskId); @@ -697,19 +656,12 @@ export async function createRouter( }) .get('/v2/tasks/:taskId/eventstream', async (req, res) => { const credentials = await httpAuth.credentials(req); + await checkPermission({ + credentials, + permissions: [taskReadPermission], + permissionService: permissions, + }); - if (permissions) { - const authorizationResponse = ( - await permissions.authorizeConditional( - [{ permission: taskReadPermission }], - { credentials: credentials }, - ) - )[0]; - - if (authorizationResponse.result === AuthorizeResult.DENY) { - throw new NotAllowedError(); - } - } const { taskId } = req.params; const after = req.query.after !== undefined ? Number(req.query.after) : undefined; @@ -759,19 +711,12 @@ export async function createRouter( }) .get('/v2/tasks/:taskId/events', async (req, res) => { const credentials = await httpAuth.credentials(req); + await checkPermission({ + credentials, + permissions: [taskReadPermission], + permissionService: permissions, + }); - if (permissions) { - const authorizationResponse = ( - await permissions.authorizeConditional( - [{ permission: taskReadPermission }], - { credentials: credentials }, - ) - )[0]; - - if (authorizationResponse.result === AuthorizeResult.DENY) { - throw new NotAllowedError(); - } - } const { taskId } = req.params; const after = Number(req.query.after) || undefined; @@ -803,19 +748,11 @@ export async function createRouter( }) .post('/v2/dry-run', async (req, res) => { const credentials = await httpAuth.credentials(req); - - if (permissions) { - const authorizationResponse = ( - await permissions.authorizeConditional( - [{ permission: taskCreatePermission }], - { credentials: credentials }, - ) - )[0]; - - if (authorizationResponse.result === AuthorizeResult.DENY) { - throw new NotAllowedError(); - } - } + await checkPermission({ + credentials, + permissions: [taskCreatePermission], + permissionService: permissions, + }); const bodySchema = z.object({ template: z.unknown(), diff --git a/plugins/scaffolder-backend/src/util/checkPermissions.ts b/plugins/scaffolder-backend/src/util/checkPermissions.ts new file mode 100644 index 0000000000..42d841ce2b --- /dev/null +++ b/plugins/scaffolder-backend/src/util/checkPermissions.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + BackstageCredentials, + PermissionsService, +} from '@backstage/backend-plugin-api'; +import { NotAllowedError } from '@backstage/errors'; +import { + AuthorizeResult, + ResourcePermission, +} from '@backstage/plugin-permission-common'; + +export type checkPermissionOptions = { + credentials: BackstageCredentials; + permissions: ResourcePermission[]; + permissionService?: PermissionsService; +}; + +/** + * Does a basic check on permissions. Throws 403 error if any permission responds with AuthorizeResult.DENY + * @public + */ +export async function checkPermission(options: checkPermissionOptions) { + const { permissions, permissionService, credentials } = options; + if (permissionService) { + const permissionRequest = permissions.map(resourcePermission => ({ + permission: resourcePermission, + })); + const authorizationResponses = await permissionService.authorizeConditional( + permissionRequest, + { credentials: credentials }, + ); + + for (const response of authorizationResponses) { + if (response.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); + } + } + } +}