From dc99b56bc8f7d13e56090770be4f52005b92b078 Mon Sep 17 00:00:00 2001 From: Deepankumar Loganathan Date: Mon, 29 Jan 2024 07:22:38 +0100 Subject: [PATCH 01/12] Azure DevOps permissions added Signed-off-by: Deepankumar Loganathan --- plugins/azure-devops-common/src/index.ts | 1 + .../azure-devops-common/src/permissions.ts | 53 +++++++++++++++++++ plugins/azure-devops-common/src/types.ts | 1 + 3 files changed, 55 insertions(+) create mode 100644 plugins/azure-devops-common/src/permissions.ts diff --git a/plugins/azure-devops-common/src/index.ts b/plugins/azure-devops-common/src/index.ts index 9a4a007317..1dd5d77d51 100644 --- a/plugins/azure-devops-common/src/index.ts +++ b/plugins/azure-devops-common/src/index.ts @@ -16,3 +16,4 @@ export * from './types'; export * from './constants'; +export * from './permissions'; diff --git a/plugins/azure-devops-common/src/permissions.ts b/plugins/azure-devops-common/src/permissions.ts new file mode 100644 index 0000000000..cd42498f07 --- /dev/null +++ b/plugins/azure-devops-common/src/permissions.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 { createPermission } from '@backstage/plugin-permission-common'; + +import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common'; + +export const azureDevOpsPullRequestReadPermission = createPermission({ + name: 'azure.devops.pullrequest.read', + attributes: { action: 'read' }, + resourceType: RESOURCE_TYPE_CATALOG_ENTITY, +}); + +export const azureDevOpsPullRequestDashboardReadPermission = createPermission({ + name: 'azure.devops.pullrequest.dashboard.read', + attributes: { action: 'read' }, +}); + +export const azureDevOpsPipelineReadPermission = createPermission({ + name: 'azure.devops.pipeline.read', + attributes: { action: 'read' }, + resourceType: RESOURCE_TYPE_CATALOG_ENTITY, +}); +export const azureDevOpsGitTagReadPermission = createPermission({ + name: 'azure.devops.gittag.read', + attributes: { action: 'read' }, + resourceType: RESOURCE_TYPE_CATALOG_ENTITY, +}); +export const azureDevOpsReadmeReadPermission = createPermission({ + name: 'azure.devops.readme.read', + attributes: { action: 'read' }, + resourceType: RESOURCE_TYPE_CATALOG_ENTITY, +}); + +export const azureDevOpsPermissions = [ + azureDevOpsPullRequestReadPermission, + azureDevOpsPipelineReadPermission, + azureDevOpsGitTagReadPermission, + azureDevOpsReadmeReadPermission, + azureDevOpsPullRequestDashboardReadPermission, +]; diff --git a/plugins/azure-devops-common/src/types.ts b/plugins/azure-devops-common/src/types.ts index 0390bb1f6e..ab53af66cb 100644 --- a/plugins/azure-devops-common/src/types.ts +++ b/plugins/azure-devops-common/src/types.ts @@ -210,6 +210,7 @@ export interface Team { export interface ReadmeConfig { project: string; repo: string; + entityRef: string; host?: string; org?: string; path?: string; From a35225da197f0936602d82ab4198d74bb9eaf9fb Mon Sep 17 00:00:00 2001 From: Deepankumar Loganathan Date: Mon, 29 Jan 2024 07:27:27 +0100 Subject: [PATCH 02/12] permissio framework add to azure-devops-backend Signed-off-by: Deepankumar Loganathan --- plugins/azure-devops-backend/package.json | 3 + .../src/service/router.test.ts | 47 +++++- .../src/service/router.ts | 150 +++++++++++++++++- yarn.lock | 5 + 4 files changed, 202 insertions(+), 3 deletions(-) diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index 9741177692..cc839412bf 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -34,9 +34,12 @@ "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-azure-devops-common": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", + "@backstage/plugin-permission-common": "workspace:^", + "@backstage/plugin-permission-node": "workspace:^", "@types/express": "^4.17.6", "azure-devops-node-api": "^12.0.0", "express": "^4.17.1", diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index c12b13a5aa..9f874923fd 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -35,10 +35,26 @@ import { createRouter } from './router'; import express from 'express'; import { getVoidLogger, UrlReaders } from '@backstage/backend-common'; import request from 'supertest'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; describe('createRouter', () => { let azureDevOpsApi: jest.Mocked; let app: express.Express; + const mockedAuthorize = jest + .fn() + .mockImplementation(async () => [{ result: AuthorizeResult.ALLOW }]); + const mockedAuthorizeConditional = jest + .fn() + .mockImplementation(async () => [{ result: AuthorizeResult.ALLOW }]); + + const mockPermissionEvaluator = { + authorize: mockedAuthorize, + authorizeConditional: mockedAuthorizeConditional, + }; + + jest.mock('@backstage/plugin-auth-node', () => ({ + getBearerTokenFromAuthorizationHeader: () => 'token', + })); beforeAll(async () => { azureDevOpsApi = { @@ -75,6 +91,7 @@ describe('createRouter', () => { config, logger, }), + permissions: mockPermissionEvaluator, }); app = express().use(router); @@ -251,7 +268,13 @@ describe('createRouter', () => { azureDevOpsApi.getGitTags.mockResolvedValueOnce(gitTags); - const response = await request(app).get('/git-tags/myProject/myRepo'); + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.ALLOW }, + ]); + + const response = await request(app) + .get('/git-tags/myProject/myRepo') + .query({ entityRef: 'component:default/mycomponent' }); expect(azureDevOpsApi.getGitTags).toHaveBeenCalledWith( 'myProject', @@ -311,10 +334,15 @@ describe('createRouter', () => { thirdPullRequest, ]; + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.ALLOW }, + ]); + azureDevOpsApi.getPullRequests.mockResolvedValueOnce(pullRequests); const response = await request(app) .get('/pull-requests/myProject/myRepo') + .query({ entityRef: 'component:default/mycomponent' }) .query({ top: '50', status: 1 }); expect(azureDevOpsApi.getPullRequests).toHaveBeenCalledWith( @@ -398,8 +426,13 @@ describe('createRouter', () => { azureDevOpsApi.getBuildRuns.mockResolvedValueOnce(buildRuns); + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.ALLOW }, + ]); + const response = await request(app) .get('/builds/myProject') + .query({ entityRef: 'component:default/mycomponent' }) .query({ top: '50', repoName: 'myRepo' }); expect(azureDevOpsApi.getBuildRuns).toHaveBeenCalledWith( @@ -453,10 +486,15 @@ describe('createRouter', () => { thirdBuildRun, ]; + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.ALLOW }, + ]); + azureDevOpsApi.getBuildRuns.mockResolvedValueOnce(buildRuns); const response = await request(app) .get('/builds/myProject') + .query({ entityRef: 'component:default/mycomponent' }) .query({ top: '50', definitionName: 'myDefinition' }); expect(azureDevOpsApi.getBuildRuns).toHaveBeenCalledWith( @@ -490,8 +528,13 @@ describe('createRouter', () => { content, url, }); + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.ALLOW }, + ]); - const response = await request(app).get('/readme/myProject/myRepo'); + const response = await request(app) + .get('/readme/myProject/myRepo?path=README.md') + .query({ entityRef: 'component:default/mycomponent' }); expect(azureDevOpsApi.getReadme).toHaveBeenCalledWith( 'host.com', 'myOrg', diff --git a/plugins/azure-devops-backend/src/service/router.ts b/plugins/azure-devops-backend/src/service/router.ts index bc13a4eee2..a77aa42c8a 100644 --- a/plugins/azure-devops-backend/src/service/router.ts +++ b/plugins/azure-devops-backend/src/service/router.ts @@ -28,6 +28,20 @@ import Router from 'express-promise-router'; import { errorHandler, UrlReader } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; import express from 'express'; +import { InputError, NotAllowedError } from '@backstage/errors'; +import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; +import { + PermissionEvaluator, + AuthorizeResult, +} from '@backstage/plugin-permission-common'; +import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; +import { + azureDevOpsPullRequestReadPermission, + azureDevOpsPermissions, + azureDevOpsPullRequestDashboardReadPermission, + azureDevOpsGitTagReadPermission, + azureDevOpsPipelineReadPermission, +} from '@backstage/plugin-azure-devops-common'; const DEFAULT_TOP = 10; @@ -37,13 +51,18 @@ export interface RouterOptions { logger: Logger; config: Config; reader: UrlReader; + permissions: PermissionEvaluator; } /** @public */ export async function createRouter( options: RouterOptions, ): Promise { - const { logger, reader, config } = options; + const { logger, reader, config, permissions } = options; + + const permissionIntegrationRouter = createPermissionIntegrationRouter({ + permissions: azureDevOpsPermissions, + }); const azureDevOpsApi = options.azureDevOpsApi || @@ -55,6 +74,8 @@ export async function createRouter( const router = Router(); router.use(express.json()); + router.use(permissionIntegrationRouter); + router.get('/health', (_req, res) => { res.status(200).json({ status: 'ok' }); }); @@ -109,6 +130,33 @@ export async function createRouter( const { projectName, repoName } = req.params; const host = req.query.host?.toString(); const org = req.query.org?.toString(); + + const entityRef = req.query.entityRef?.toString(); + if (typeof entityRef !== 'string') { + throw new InputError('Invalid entityRef, not a string'); + } + + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); + const decision = ( + await permissions.authorize( + [ + { + permission: azureDevOpsGitTagReadPermission, + resourceRef: entityRef, + }, + ], + { + token, + }, + ) + )[0]; + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError('Unauthorized'); + } + const gitTags = await azureDevOpsApi.getGitTags( projectName, repoName, @@ -133,6 +181,32 @@ export async function createRouter( status: status, }; + const entityRef = req.query.entityRef?.toString(); + if (typeof entityRef !== 'string') { + throw new InputError('Invalid entityRef, not a string'); + } + + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); + const decision = ( + await permissions.authorize( + [ + { + permission: azureDevOpsPullRequestReadPermission, + resourceRef: entityRef, + }, + ], + { + token, + }, + ) + )[0]; + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError('Unauthorized'); + } + const gitPullRequest = await azureDevOpsApi.getPullRequests( projectName, repoName, @@ -158,6 +232,26 @@ export async function createRouter( status: status, }; + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); + const decision = ( + await permissions.authorize( + [ + { + permission: azureDevOpsPullRequestDashboardReadPermission, + }, + ], + { + token, + }, + ) + )[0]; + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError('Unauthorized'); + } + const pullRequests: DashboardPullRequest[] = await pullRequestsDashboardProvider.getDashboardPullRequests( projectName, @@ -195,6 +289,33 @@ export async function createRouter( const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP; const host = req.query.host?.toString(); const org = req.query.org?.toString(); + + const entityRef = req.query.entityRef?.toString(); + if (typeof entityRef !== 'string') { + throw new InputError('Invalid entityRef, not a string'); + } + + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); + const decision = ( + await permissions.authorize( + [ + { + permission: azureDevOpsPipelineReadPermission, + resourceRef: entityRef, + }, + ], + { + token, + }, + ) + )[0]; + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError('Unauthorized'); + } + const builds = await azureDevOpsApi.getBuildRuns( projectName, top, @@ -233,6 +354,33 @@ export async function createRouter( } const { projectName, repoName } = req.params; + + const entityRef = req.query.entityRef?.toString(); + if (typeof entityRef !== 'string') { + throw new InputError('Invalid entityRef, not a string'); + } + + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); + const decision = ( + await permissions.authorize( + [ + { + permission: azureDevOpsPullRequestReadPermission, + resourceRef: entityRef, + }, + ], + { + token, + }, + ) + )[0]; + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError('Unauthorized'); + } + const readme = await azureDevOpsApi.getReadme( host, org, diff --git a/yarn.lock b/yarn.lock index 3c08b1e93e..d0b27cb311 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5120,9 +5120,12 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-azure-devops-common": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" + "@backstage/plugin-permission-node": "workspace:^" "@types/express": ^4.17.6 "@types/supertest": ^2.0.8 azure-devops-node-api: ^12.0.0 @@ -5142,6 +5145,8 @@ __metadata: resolution: "@backstage/plugin-azure-devops-common@workspace:plugins/azure-devops-common" dependencies: "@backstage/cli": "workspace:^" + "@backstage/plugin-catalog-common": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" languageName: unknown linkType: soft From b433e52d5f74adf096755f5b40c6544ab38bad52 Mon Sep 17 00:00:00 2001 From: Deepankumar Loganathan Date: Mon, 29 Jan 2024 07:29:02 +0100 Subject: [PATCH 03/12] permissio framework add to azure-devops Signed-off-by: Deepankumar Loganathan --- plugins/azure-devops-common/package.json | 6 +++++- plugins/azure-devops/src/api/AzureDevOpsApi.ts | 3 +++ .../azure-devops/src/api/AzureDevOpsClient.ts | 6 ++++++ .../src/components/BuildTable/BuildTable.tsx | 11 +++++++++++ .../src/components/GitTagTable/GitTagTable.tsx | 11 +++++++++++ .../PullRequestTable/PullRequestTable.tsx | 11 +++++++++++ .../PullRequestsPage/PullRequestsPage.tsx | 11 +++++++++++ .../src/components/ReadmeCard/ReadmeCard.tsx | 12 +++++++++++- plugins/azure-devops/src/hooks/useBuildRuns.ts | 12 ++++++++++-- plugins/azure-devops/src/hooks/useGitTags.ts | 10 ++++++++-- .../azure-devops/src/hooks/usePullRequests.ts | 12 ++++++++++-- plugins/azure-devops/src/hooks/useReadme.ts | 4 +++- plugins/azure-devops/src/utils/index.ts | 1 + .../azure-devops/src/utils/validateError.ts | 18 ++++++++++++++++++ 14 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 plugins/azure-devops/src/utils/validateError.ts diff --git a/plugins/azure-devops-common/package.json b/plugins/azure-devops-common/package.json index 23a575f95e..70b7be4964 100644 --- a/plugins/azure-devops-common/package.json +++ b/plugins/azure-devops-common/package.json @@ -36,5 +36,9 @@ }, "files": [ "dist" - ] + ], + "dependencies": { + "@backstage/plugin-catalog-common": "workspace:^", + "@backstage/plugin-permission-common": "workspace:^" + } } diff --git a/plugins/azure-devops/src/api/AzureDevOpsApi.ts b/plugins/azure-devops/src/api/AzureDevOpsApi.ts index 89aa4b7d6e..6dd6eeae6b 100644 --- a/plugins/azure-devops/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops/src/api/AzureDevOpsApi.ts @@ -48,6 +48,7 @@ export interface AzureDevOpsApi { getGitTags( projectName: string, repoName: string, + entityRef: string, host?: string, org?: string, ): Promise<{ items: GitTag[] }>; @@ -55,6 +56,7 @@ export interface AzureDevOpsApi { getPullRequests( projectName: string, repoName: string, + entityRef: string, host?: string, org?: string, options?: PullRequestOptions, @@ -70,6 +72,7 @@ export interface AzureDevOpsApi { getBuildRuns( projectName: string, + entityRef: string, repoName?: string, definitionName?: string, host?: string, diff --git a/plugins/azure-devops/src/api/AzureDevOpsClient.ts b/plugins/azure-devops/src/api/AzureDevOpsClient.ts index f5c046ef07..257b1cd014 100644 --- a/plugins/azure-devops/src/api/AzureDevOpsClient.ts +++ b/plugins/azure-devops/src/api/AzureDevOpsClient.ts @@ -71,6 +71,7 @@ export class AzureDevOpsClient implements AzureDevOpsApi { public async getGitTags( projectName: string, + entityRef: string, repoName: string, host?: string, org?: string, @@ -82,6 +83,7 @@ export class AzureDevOpsClient implements AzureDevOpsApi { if (org) { queryString.append('org', org); } + queryString.append('entityRef', entityRef); const urlSegment = `git-tags/${encodeURIComponent( projectName, )}/${encodeURIComponent(repoName)}?${queryString}`; @@ -93,6 +95,7 @@ export class AzureDevOpsClient implements AzureDevOpsApi { public async getPullRequests( projectName: string, repoName: string, + entityRef: string, host?: string, org?: string, options?: PullRequestOptions, @@ -110,6 +113,7 @@ export class AzureDevOpsClient implements AzureDevOpsApi { if (org) { queryString.append('org', org); } + queryString.append('entityRef', entityRef); const urlSegment = `pull-requests/${encodeURIComponent( projectName, )}/${encodeURIComponent(repoName)}?${queryString}`; @@ -136,6 +140,7 @@ export class AzureDevOpsClient implements AzureDevOpsApi { public async getBuildRuns( projectName: string, + entityRef: string, repoName?: string, definitionName?: string, host?: string, @@ -174,6 +179,7 @@ export class AzureDevOpsClient implements AzureDevOpsApi { if (options?.top) { queryString.append('top', options.top.toString()); } + queryString.append('entityRef', entityRef); const urlSegment = `builds/${encodeURIComponent( projectName, )}?${queryString}`; diff --git a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx index 09eba5f5a2..bc69b68d56 100644 --- a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx +++ b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx @@ -32,12 +32,14 @@ import { StatusWarning, Table, TableColumn, + EmptyState, } from '@backstage/core-components'; import { AzurePipelinesIcon } from '../AzurePipelinesIcon'; import { DateTime } from 'luxon'; import React from 'react'; import { getDurationFromDates } from '../../utils/getDurationFromDates'; +import { isAuthorizationError } from '../../utils'; export const getBuildResultComponent = (result: number | undefined) => { switch (result) { @@ -179,6 +181,15 @@ type BuildTableProps = { export const BuildTable = ({ items, loading, error }: BuildTableProps) => { if (error) { + if (isAuthorizationError(error)) { + return ( + + ); + } return (
diff --git a/plugins/azure-devops/src/components/GitTagTable/GitTagTable.tsx b/plugins/azure-devops/src/components/GitTagTable/GitTagTable.tsx index f34a1929fe..31475a66d7 100644 --- a/plugins/azure-devops/src/components/GitTagTable/GitTagTable.tsx +++ b/plugins/azure-devops/src/components/GitTagTable/GitTagTable.tsx @@ -20,6 +20,7 @@ import { ResponseErrorPanel, Table, TableColumn, + EmptyState, } from '@backstage/core-components'; import { GitTag } from '@backstage/plugin-azure-devops-common'; import React from 'react'; @@ -27,6 +28,7 @@ import React from 'react'; import { AzureGitTagsIcon } from '../AzureGitTagsIcon'; import { useEntity } from '@backstage/plugin-catalog-react'; import { useGitTags } from '../../hooks/useGitTags'; +import { isAuthorizationError } from '../../utils'; const columns: TableColumn[] = [ { @@ -64,6 +66,15 @@ export const GitTagTable = () => { const { items, loading, error } = useGitTags(entity); if (error) { + if (isAuthorizationError(error)) { + return ( + + ); + } return ; } diff --git a/plugins/azure-devops/src/components/PullRequestTable/PullRequestTable.tsx b/plugins/azure-devops/src/components/PullRequestTable/PullRequestTable.tsx index d672102945..3e41a8c46f 100644 --- a/plugins/azure-devops/src/components/PullRequestTable/PullRequestTable.tsx +++ b/plugins/azure-devops/src/components/PullRequestTable/PullRequestTable.tsx @@ -21,6 +21,7 @@ import { ResponseErrorPanel, Table, TableColumn, + EmptyState, } from '@backstage/core-components'; import { PullRequest, @@ -33,6 +34,7 @@ import { DateTime } from 'luxon'; import { PullRequestStatusButtonGroup } from '../PullRequestStatusButtonGroup'; import { useEntity } from '@backstage/plugin-catalog-react'; import { usePullRequests } from '../../hooks/usePullRequests'; +import { isAuthorizationError } from '../../utils'; const columns: TableColumn[] = [ { @@ -104,6 +106,15 @@ export const PullRequestTable = ({ defaultLimit }: PullRequestTableProps) => { ); if (error) { + if (isAuthorizationError(error)) { + return ( + + ); + } return ; } diff --git a/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx b/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx index c5da207bd6..9561c76a2c 100644 --- a/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx +++ b/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx @@ -16,6 +16,7 @@ import { Content, + EmptyState, Header, Page, Progress, @@ -28,6 +29,7 @@ import { FilterType } from './lib/filters'; import { PullRequestGrid } from './lib/PullRequestGrid'; import { useDashboardPullRequests } from '../../hooks'; import { useFilterProcessor } from './lib/hooks'; +import { isAuthorizationError } from '../../utils'; type PullRequestsPageContentProps = { pullRequestGroups: PullRequestGroup[] | undefined; @@ -45,6 +47,15 @@ const PullRequestsPageContent = ({ } if (error) { + if (isAuthorizationError(error)) { + return ( + + ); + } return ; } diff --git a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx index 300c9e0d7c..a45c6fe831 100644 --- a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx +++ b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx @@ -28,6 +28,7 @@ import { useEntity } from '@backstage/plugin-catalog-react'; import React from 'react'; import { useReadme } from '../../hooks'; +import { isAuthorizationError } from '../../utils'; const useStyles = makeStyles(theme => ({ readMe: { @@ -63,7 +64,15 @@ function isNotFoundError(error: any): boolean { } const ReadmeCardError = ({ error }: ErrorProps) => { - if (isNotFoundError(error)) + if (isAuthorizationError(error)) { + return ( + + ); + } else if (isNotFoundError(error)) { return ( { } /> ); + } return ; }; diff --git a/plugins/azure-devops/src/hooks/useBuildRuns.ts b/plugins/azure-devops/src/hooks/useBuildRuns.ts index e335d2ffaf..21f4d60430 100644 --- a/plugins/azure-devops/src/hooks/useBuildRuns.ts +++ b/plugins/azure-devops/src/hooks/useBuildRuns.ts @@ -24,7 +24,7 @@ import { azureDevOpsApiRef } from '../api'; import { useApi } from '@backstage/core-plugin-api'; import useAsync from 'react-use/lib/useAsync'; import { getAnnotationValuesFromEntity } from '../utils'; -import { Entity } from '@backstage/catalog-model'; +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; export function useBuildRuns( entity: Entity, @@ -44,7 +44,15 @@ export function useBuildRuns( const { value, loading, error } = useAsync(() => { const { project, repo, definition, host, org } = getAnnotationValuesFromEntity(entity); - return api.getBuildRuns(project, repo, definition, host, org, options); + return api.getBuildRuns( + project, + stringifyEntityRef(entity), + repo, + definition, + host, + org, + options, + ); }, [api]); return { diff --git a/plugins/azure-devops/src/hooks/useGitTags.ts b/plugins/azure-devops/src/hooks/useGitTags.ts index 57489316da..3cd65dad92 100644 --- a/plugins/azure-devops/src/hooks/useGitTags.ts +++ b/plugins/azure-devops/src/hooks/useGitTags.ts @@ -16,7 +16,7 @@ import { GitTag } from '@backstage/plugin-azure-devops-common'; -import { Entity } from '@backstage/catalog-model'; +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { azureDevOpsApiRef } from '../api'; import { useApi } from '@backstage/core-plugin-api'; import useAsync from 'react-use/lib/useAsync'; @@ -31,7 +31,13 @@ export function useGitTags(entity: Entity): { const { value, loading, error } = useAsync(() => { const { project, repo, host, org } = getAnnotationValuesFromEntity(entity); - return api.getGitTags(project, repo as string, host, org); + return api.getGitTags( + project, + repo as string, + stringifyEntityRef(entity), + host, + org, + ); }, [api]); return { diff --git a/plugins/azure-devops/src/hooks/usePullRequests.ts b/plugins/azure-devops/src/hooks/usePullRequests.ts index af2c4b1cc1..7347f66156 100644 --- a/plugins/azure-devops/src/hooks/usePullRequests.ts +++ b/plugins/azure-devops/src/hooks/usePullRequests.ts @@ -21,7 +21,7 @@ import { PullRequestStatus, } from '@backstage/plugin-azure-devops-common'; -import { Entity } from '@backstage/catalog-model'; +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { azureDevOpsApiRef } from '../api'; import { useApi } from '@backstage/core-plugin-api'; import useAsync from 'react-use/lib/useAsync'; @@ -47,7 +47,15 @@ export function usePullRequests( const { value, loading, error } = useAsync(() => { const { project, repo, host, org } = getAnnotationValuesFromEntity(entity); - return api.getPullRequests(project, repo as string, host, org, options); + const entityRef = stringifyEntityRef(entity); + return api.getPullRequests( + project, + repo as string, + entityRef, + host, + org, + options, + ); }, [api, top, status]); return { diff --git a/plugins/azure-devops/src/hooks/useReadme.ts b/plugins/azure-devops/src/hooks/useReadme.ts index 9aa376e0bd..cfc7f04bf4 100644 --- a/plugins/azure-devops/src/hooks/useReadme.ts +++ b/plugins/azure-devops/src/hooks/useReadme.ts @@ -16,7 +16,7 @@ import { Readme } from '@backstage/plugin-azure-devops-common'; -import { Entity } from '@backstage/catalog-model'; +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { azureDevOpsApiRef } from '../api'; import { useApi } from '@backstage/core-plugin-api'; import useAsync from 'react-use/lib/useAsync'; @@ -32,9 +32,11 @@ export function useReadme(entity: Entity): { const { value, loading, error } = useAsync(() => { const { project, repo, host, org, readmePath } = getAnnotationValuesFromEntity(entity); + const entityRef = stringifyEntityRef(entity); return api.getReadme({ project, repo: repo as string, + entityRef, host, org, path: readmePath, diff --git a/plugins/azure-devops/src/utils/index.ts b/plugins/azure-devops/src/utils/index.ts index 998bb0e23d..a05ea4f650 100644 --- a/plugins/azure-devops/src/utils/index.ts +++ b/plugins/azure-devops/src/utils/index.ts @@ -18,3 +18,4 @@ export * from './arrayHas'; export * from './equalsIgnoreCase'; export * from './getDurationFromDates'; export * from './getAnnotationValuesFromEntity'; +export * from './validateError'; diff --git a/plugins/azure-devops/src/utils/validateError.ts b/plugins/azure-devops/src/utils/validateError.ts new file mode 100644 index 0000000000..fca1687013 --- /dev/null +++ b/plugins/azure-devops/src/utils/validateError.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ +export function isAuthorizationError(error: any): boolean { + return error?.response?.status === 403; +} From 6d7a92759c2ce6eb17682e2ba8fa10df3d30d74f Mon Sep 17 00:00:00 2001 From: Deepankumar Loganathan Date: Mon, 29 Jan 2024 20:08:46 +0100 Subject: [PATCH 04/12] minor changes Signed-off-by: Deepankumar Loganathan --- plugins/azure-devops-backend/src/plugin.ts | 4 +++- plugins/azure-devops-common/src/permissions.ts | 2 +- plugins/azure-devops/src/api/AzureDevOpsClient.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/azure-devops-backend/src/plugin.ts b/plugins/azure-devops-backend/src/plugin.ts index ea0976a1f2..1cf17ceecc 100644 --- a/plugins/azure-devops-backend/src/plugin.ts +++ b/plugins/azure-devops-backend/src/plugin.ts @@ -34,14 +34,16 @@ export const azureDevOpsPlugin = createBackendPlugin({ config: coreServices.rootConfig, logger: coreServices.logger, reader: coreServices.urlReader, + permissions: coreServices.permissions, httpRouter: coreServices.httpRouter, }, - async init({ config, logger, reader, httpRouter }) { + async init({ config, logger, reader, permissions, httpRouter }) { httpRouter.use( await createRouter({ config, logger: loggerToWinstonLogger(logger), reader, + permissions, }), ); }, diff --git a/plugins/azure-devops-common/src/permissions.ts b/plugins/azure-devops-common/src/permissions.ts index cd42498f07..86e1d26138 100644 --- a/plugins/azure-devops-common/src/permissions.ts +++ b/plugins/azure-devops-common/src/permissions.ts @@ -15,7 +15,7 @@ */ import { createPermission } from '@backstage/plugin-permission-common'; -import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common'; +import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; export const azureDevOpsPullRequestReadPermission = createPermission({ name: 'azure.devops.pullrequest.read', diff --git a/plugins/azure-devops/src/api/AzureDevOpsClient.ts b/plugins/azure-devops/src/api/AzureDevOpsClient.ts index 257b1cd014..e1fde33555 100644 --- a/plugins/azure-devops/src/api/AzureDevOpsClient.ts +++ b/plugins/azure-devops/src/api/AzureDevOpsClient.ts @@ -71,8 +71,8 @@ export class AzureDevOpsClient implements AzureDevOpsApi { public async getGitTags( projectName: string, - entityRef: string, repoName: string, + entityRef: string, host?: string, org?: string, ): Promise<{ items: GitTag[] }> { From 3270b1b4f8c571aa9f6911ccefd0ae8f0142d7bf Mon Sep 17 00:00:00 2001 From: Deepankumar Loganathan Date: Mon, 29 Jan 2024 21:40:11 +0100 Subject: [PATCH 05/12] api-reports and changeset added Signed-off-by: Deepankumar Loganathan --- .changeset/ten-dryers-compete.md | 13 +++++++ packages/backend/src/plugins/azure-devops.ts | 1 + plugins/azure-devops-backend/api-report.md | 3 ++ .../src/service/standaloneServer.ts | 13 +++++++ plugins/azure-devops-common/api-report.md | 38 +++++++++++++++++++ plugins/azure-devops/api-report.md | 6 +++ 6 files changed, 74 insertions(+) create mode 100644 .changeset/ten-dryers-compete.md diff --git a/.changeset/ten-dryers-compete.md b/.changeset/ten-dryers-compete.md new file mode 100644 index 0000000000..14fdcd1e6c --- /dev/null +++ b/.changeset/ten-dryers-compete.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-azure-devops-backend': minor +'@backstage/plugin-azure-devops': minor +'@backstage/plugin-azure-devops-common': patch +--- + +Azure DevOps plugin is now integrated with permission framework for the below features + +1. PullRequest +2. Dashboard Pull Request +3. Pipeline +4. GitTag +5. Readme diff --git a/packages/backend/src/plugins/azure-devops.ts b/packages/backend/src/plugins/azure-devops.ts index ed5b1d0068..719bb0500e 100644 --- a/packages/backend/src/plugins/azure-devops.ts +++ b/packages/backend/src/plugins/azure-devops.ts @@ -25,5 +25,6 @@ export default async function createPlugin( logger: env.logger, config: env.config, reader: env.reader, + permissions: env.permissions, }); } diff --git a/plugins/azure-devops-backend/api-report.md b/plugins/azure-devops-backend/api-report.md index d98d178cff..7231509fc1 100644 --- a/plugins/azure-devops-backend/api-report.md +++ b/plugins/azure-devops-backend/api-report.md @@ -16,6 +16,7 @@ import { GitRepository } from 'azure-devops-node-api/interfaces/GitInterfaces'; import { GitTag } from '@backstage/plugin-azure-devops-common'; import { LocationSpec } from '@backstage/plugin-catalog-common'; import { Logger } from 'winston'; +import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { Project } from '@backstage/plugin-azure-devops-common'; import { PullRequest } from '@backstage/plugin-azure-devops-common'; import { PullRequestOptions } from '@backstage/plugin-azure-devops-common'; @@ -160,6 +161,8 @@ export interface RouterOptions { // (undocumented) logger: Logger; // (undocumented) + permissions: PermissionEvaluator; + // (undocumented) reader: UrlReader; } ``` diff --git a/plugins/azure-devops-backend/src/service/standaloneServer.ts b/plugins/azure-devops-backend/src/service/standaloneServer.ts index e2f4aa4986..65b13fab35 100644 --- a/plugins/azure-devops-backend/src/service/standaloneServer.ts +++ b/plugins/azure-devops-backend/src/service/standaloneServer.ts @@ -18,10 +18,13 @@ import { createServiceBuilder, loadBackendConfig, UrlReaders, + ServerTokenManager, + HostDiscovery, } from '@backstage/backend-common'; import { Server } from 'http'; import { Logger } from 'winston'; import { createRouter } from './router'; +import { ServerPermissionClient } from '@backstage/plugin-permission-node'; export interface ServerOptions { port: number; @@ -34,13 +37,23 @@ export async function startStandaloneServer( ): Promise { const logger = options.logger.child({ service: 'azure-devops-backend' }); const config = await loadBackendConfig({ logger, argv: process.argv }); + const discovery = HostDiscovery.fromConfig(config); logger.debug('Starting application server...'); + const tokenManager = ServerTokenManager.fromConfig(config, { + logger, + }); + const permissions = ServerPermissionClient.fromConfig(config, { + discovery, + tokenManager, + }); + const router = await createRouter({ logger, config, reader: UrlReaders.default({ logger, config }), + permissions, }); let service = createServiceBuilder(module) diff --git a/plugins/azure-devops-common/api-report.md b/plugins/azure-devops-common/api-report.md index e8343178e3..7e559ac4fc 100644 --- a/plugins/azure-devops-common/api-report.md +++ b/plugins/azure-devops-common/api-report.md @@ -3,6 +3,9 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BasicPermission } from '@backstage/plugin-permission-common'; +import { ResourcePermission } from '@backstage/plugin-permission-common'; + // @public (undocumented) export const AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION = 'dev.azure.com/build-definition'; @@ -22,6 +25,39 @@ export const AZURE_DEVOPS_README_ANNOTATION = 'dev.azure.com/readme-path'; // @public (undocumented) export const AZURE_DEVOPS_REPO_ANNOTATION = 'dev.azure.com/project-repo'; +// Warning: (ae-missing-release-tag) "azureDevOpsGitTagReadPermission" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const azureDevOpsGitTagReadPermission: ResourcePermission<'catalog-entity'>; + +// Warning: (ae-missing-release-tag) "azureDevOpsPermissions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const azureDevOpsPermissions: ( + | BasicPermission + | ResourcePermission<'catalog-entity'> +)[]; + +// Warning: (ae-missing-release-tag) "azureDevOpsPipelineReadPermission" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const azureDevOpsPipelineReadPermission: ResourcePermission<'catalog-entity'>; + +// Warning: (ae-missing-release-tag) "azureDevOpsPullRequestDashboardReadPermission" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const azureDevOpsPullRequestDashboardReadPermission: BasicPermission; + +// Warning: (ae-missing-release-tag) "azureDevOpsPullRequestReadPermission" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const azureDevOpsPullRequestReadPermission: ResourcePermission<'catalog-entity'>; + +// Warning: (ae-missing-release-tag) "azureDevOpsReadmeReadPermission" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const azureDevOpsReadmeReadPermission: ResourcePermission<'catalog-entity'>; + // @public (undocumented) export enum BuildResult { Canceled = 32, @@ -226,6 +262,8 @@ export interface Readme { // @public (undocumented) export interface ReadmeConfig { + // (undocumented) + entityRef: string; // (undocumented) host?: string; // (undocumented) diff --git a/plugins/azure-devops/api-report.md b/plugins/azure-devops/api-report.md index e4e2d4306f..e76e172d9b 100644 --- a/plugins/azure-devops/api-report.md +++ b/plugins/azure-devops/api-report.md @@ -69,6 +69,7 @@ export interface AzureDevOpsApi { // (undocumented) getBuildRuns( projectName: string, + entityRef: string, repoName?: string, definitionName?: string, host?: string, @@ -85,6 +86,7 @@ export interface AzureDevOpsApi { getGitTags( projectName: string, repoName: string, + entityRef: string, host?: string, org?: string, ): Promise<{ @@ -94,6 +96,7 @@ export interface AzureDevOpsApi { getPullRequests( projectName: string, repoName: string, + entityRef: string, host?: string, org?: string, options?: PullRequestOptions, @@ -127,6 +130,7 @@ export class AzureDevOpsClient implements AzureDevOpsApi { // (undocumented) getBuildRuns( projectName: string, + entityRef: string, repoName?: string, definitionName?: string, host?: string, @@ -143,6 +147,7 @@ export class AzureDevOpsClient implements AzureDevOpsApi { getGitTags( projectName: string, repoName: string, + entityRef: string, host?: string, org?: string, ): Promise<{ @@ -152,6 +157,7 @@ export class AzureDevOpsClient implements AzureDevOpsApi { getPullRequests( projectName: string, repoName: string, + entityRef: string, host?: string, org?: string, options?: PullRequestOptions, From 2bfd8ac15bbe24fda259f2d4c2c156af226ba861 Mon Sep 17 00:00:00 2001 From: Deepankumar Loganathan Date: Wed, 31 Jan 2024 07:05:19 +0100 Subject: [PATCH 06/12] frontend permission integration changed Signed-off-by: Deepankumar Loganathan --- .changeset/ten-dryers-compete.md | 8 +--- plugins/azure-devops-common/api-report.md | 12 ----- .../azure-devops-common/src/permissions.ts | 21 ++++++++- plugins/azure-devops/package.json | 1 + .../src/components/BuildTable/BuildTable.tsx | 11 ----- .../EntityPageAzureGitTags.tsx | 24 +++++++++- .../EntityPageAzurePipelines.tsx | 20 +++++++- .../EntityPageAzurePullRequests.tsx | 22 ++++++++- .../components/GitTagTable/GitTagTable.tsx | 11 ----- .../PullRequestTable/PullRequestTable.tsx | 11 ----- .../PullRequestsPage/PullRequestsPage.tsx | 11 ----- .../src/components/ReadmeCard/ReadmeCard.tsx | 46 +++++++++++-------- plugins/azure-devops/src/utils/index.ts | 1 - .../azure-devops/src/utils/validateError.ts | 18 -------- yarn.lock | 1 + 15 files changed, 112 insertions(+), 106 deletions(-) delete mode 100644 plugins/azure-devops/src/utils/validateError.ts diff --git a/.changeset/ten-dryers-compete.md b/.changeset/ten-dryers-compete.md index 14fdcd1e6c..02715d0736 100644 --- a/.changeset/ten-dryers-compete.md +++ b/.changeset/ten-dryers-compete.md @@ -4,10 +4,4 @@ '@backstage/plugin-azure-devops-common': patch --- -Azure DevOps plugin is now integrated with permission framework for the below features - -1. PullRequest -2. Dashboard Pull Request -3. Pipeline -4. GitTag -5. Readme +Azure DevOps plugin is now integrated with permission framework for its core features, see the https://github.com/backstage/backstage/blob/master/plugins/azure-devops/README.md for more details. diff --git a/plugins/azure-devops-common/api-report.md b/plugins/azure-devops-common/api-report.md index 7e559ac4fc..c926823367 100644 --- a/plugins/azure-devops-common/api-report.md +++ b/plugins/azure-devops-common/api-report.md @@ -25,36 +25,24 @@ export const AZURE_DEVOPS_README_ANNOTATION = 'dev.azure.com/readme-path'; // @public (undocumented) export const AZURE_DEVOPS_REPO_ANNOTATION = 'dev.azure.com/project-repo'; -// Warning: (ae-missing-release-tag) "azureDevOpsGitTagReadPermission" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const azureDevOpsGitTagReadPermission: ResourcePermission<'catalog-entity'>; -// Warning: (ae-missing-release-tag) "azureDevOpsPermissions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const azureDevOpsPermissions: ( | BasicPermission | ResourcePermission<'catalog-entity'> )[]; -// Warning: (ae-missing-release-tag) "azureDevOpsPipelineReadPermission" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const azureDevOpsPipelineReadPermission: ResourcePermission<'catalog-entity'>; -// Warning: (ae-missing-release-tag) "azureDevOpsPullRequestDashboardReadPermission" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const azureDevOpsPullRequestDashboardReadPermission: BasicPermission; -// Warning: (ae-missing-release-tag) "azureDevOpsPullRequestReadPermission" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const azureDevOpsPullRequestReadPermission: ResourcePermission<'catalog-entity'>; -// Warning: (ae-missing-release-tag) "azureDevOpsReadmeReadPermission" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const azureDevOpsReadmeReadPermission: ResourcePermission<'catalog-entity'>; diff --git a/plugins/azure-devops-common/src/permissions.ts b/plugins/azure-devops-common/src/permissions.ts index 86e1d26138..da1515580a 100644 --- a/plugins/azure-devops-common/src/permissions.ts +++ b/plugins/azure-devops-common/src/permissions.ts @@ -14,36 +14,55 @@ * limitations under the License. */ import { createPermission } from '@backstage/plugin-permission-common'; - import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; +/** + * @public + */ export const azureDevOpsPullRequestReadPermission = createPermission({ name: 'azure.devops.pullrequest.read', attributes: { action: 'read' }, resourceType: RESOURCE_TYPE_CATALOG_ENTITY, }); +/** + * @public + */ export const azureDevOpsPullRequestDashboardReadPermission = createPermission({ name: 'azure.devops.pullrequest.dashboard.read', attributes: { action: 'read' }, }); +/** + * @public + */ export const azureDevOpsPipelineReadPermission = createPermission({ name: 'azure.devops.pipeline.read', attributes: { action: 'read' }, resourceType: RESOURCE_TYPE_CATALOG_ENTITY, }); + +/** + * @public + */ export const azureDevOpsGitTagReadPermission = createPermission({ name: 'azure.devops.gittag.read', attributes: { action: 'read' }, resourceType: RESOURCE_TYPE_CATALOG_ENTITY, }); + +/** + * @public + */ export const azureDevOpsReadmeReadPermission = createPermission({ name: 'azure.devops.readme.read', attributes: { action: 'read' }, resourceType: RESOURCE_TYPE_CATALOG_ENTITY, }); +/** + * @public + */ export const azureDevOpsPermissions = [ azureDevOpsPullRequestReadPermission, azureDevOpsPipelineReadPermission, diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index 12860d82cc..6e72c4e45d 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -50,6 +50,7 @@ "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-azure-devops-common": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-permission-react": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", diff --git a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx index bc69b68d56..09eba5f5a2 100644 --- a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx +++ b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx @@ -32,14 +32,12 @@ import { StatusWarning, Table, TableColumn, - EmptyState, } from '@backstage/core-components'; import { AzurePipelinesIcon } from '../AzurePipelinesIcon'; import { DateTime } from 'luxon'; import React from 'react'; import { getDurationFromDates } from '../../utils/getDurationFromDates'; -import { isAuthorizationError } from '../../utils'; export const getBuildResultComponent = (result: number | undefined) => { switch (result) { @@ -181,15 +179,6 @@ type BuildTableProps = { export const BuildTable = ({ items, loading, error }: BuildTableProps) => { if (error) { - if (isAuthorizationError(error)) { - return ( - - ); - } return (
diff --git a/plugins/azure-devops/src/components/EntityPageAzureGitTags/EntityPageAzureGitTags.tsx b/plugins/azure-devops/src/components/EntityPageAzureGitTags/EntityPageAzureGitTags.tsx index 2d94d735df..1632649d59 100644 --- a/plugins/azure-devops/src/components/EntityPageAzureGitTags/EntityPageAzureGitTags.tsx +++ b/plugins/azure-devops/src/components/EntityPageAzureGitTags/EntityPageAzureGitTags.tsx @@ -14,7 +14,29 @@ * limitations under the License. */ +import { azureDevOpsGitTagReadPermission } from '@backstage/plugin-azure-devops-common'; import { GitTagTable } from '../GitTagTable/GitTagTable'; import React from 'react'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { RequirePermission } from '@backstage/plugin-permission-react'; +import { EmptyState } from '@backstage/core-components'; -export const EntityPageAzureGitTags = () => ; +export const EntityPageAzureGitTags = () => { + const { entity } = useEntity(); + return ( + + } + > + + + ); +}; diff --git a/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx b/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx index c33d849c24..39e3da1a7e 100644 --- a/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx +++ b/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx @@ -18,11 +18,29 @@ import { BuildTable } from '../BuildTable/BuildTable'; import React from 'react'; import { useBuildRuns } from '../../hooks'; import { useEntity } from '@backstage/plugin-catalog-react'; +import { azureDevOpsPipelineReadPermission } from '@backstage/plugin-azure-devops-common'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { RequirePermission } from '@backstage/plugin-permission-react'; +import { EmptyState } from '@backstage/core-components'; export const EntityPageAzurePipelines = (props: { defaultLimit?: number }) => { const { entity } = useEntity(); const { items, loading, error } = useBuildRuns(entity, props.defaultLimit); - return ; + return ( + + } + > + + + ); }; diff --git a/plugins/azure-devops/src/components/EntityPageAzurePullRequests/EntityPageAzurePullRequests.tsx b/plugins/azure-devops/src/components/EntityPageAzurePullRequests/EntityPageAzurePullRequests.tsx index b5a21ff750..7b828c512c 100644 --- a/plugins/azure-devops/src/components/EntityPageAzurePullRequests/EntityPageAzurePullRequests.tsx +++ b/plugins/azure-devops/src/components/EntityPageAzurePullRequests/EntityPageAzurePullRequests.tsx @@ -14,11 +14,31 @@ * limitations under the License. */ +import { azureDevOpsPullRequestReadPermission } from '@backstage/plugin-azure-devops-common'; import { PullRequestTable } from '../PullRequestTable/PullRequestTable'; import React from 'react'; +import { RequirePermission } from '@backstage/plugin-permission-react'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { EmptyState } from '@backstage/core-components'; export const EntityPageAzurePullRequests = (props: { defaultLimit?: number; }) => { - return ; + const { entity } = useEntity(); + return ( + + } + > + + + ); }; diff --git a/plugins/azure-devops/src/components/GitTagTable/GitTagTable.tsx b/plugins/azure-devops/src/components/GitTagTable/GitTagTable.tsx index 31475a66d7..f34a1929fe 100644 --- a/plugins/azure-devops/src/components/GitTagTable/GitTagTable.tsx +++ b/plugins/azure-devops/src/components/GitTagTable/GitTagTable.tsx @@ -20,7 +20,6 @@ import { ResponseErrorPanel, Table, TableColumn, - EmptyState, } from '@backstage/core-components'; import { GitTag } from '@backstage/plugin-azure-devops-common'; import React from 'react'; @@ -28,7 +27,6 @@ import React from 'react'; import { AzureGitTagsIcon } from '../AzureGitTagsIcon'; import { useEntity } from '@backstage/plugin-catalog-react'; import { useGitTags } from '../../hooks/useGitTags'; -import { isAuthorizationError } from '../../utils'; const columns: TableColumn[] = [ { @@ -66,15 +64,6 @@ export const GitTagTable = () => { const { items, loading, error } = useGitTags(entity); if (error) { - if (isAuthorizationError(error)) { - return ( - - ); - } return ; } diff --git a/plugins/azure-devops/src/components/PullRequestTable/PullRequestTable.tsx b/plugins/azure-devops/src/components/PullRequestTable/PullRequestTable.tsx index 3e41a8c46f..d672102945 100644 --- a/plugins/azure-devops/src/components/PullRequestTable/PullRequestTable.tsx +++ b/plugins/azure-devops/src/components/PullRequestTable/PullRequestTable.tsx @@ -21,7 +21,6 @@ import { ResponseErrorPanel, Table, TableColumn, - EmptyState, } from '@backstage/core-components'; import { PullRequest, @@ -34,7 +33,6 @@ import { DateTime } from 'luxon'; import { PullRequestStatusButtonGroup } from '../PullRequestStatusButtonGroup'; import { useEntity } from '@backstage/plugin-catalog-react'; import { usePullRequests } from '../../hooks/usePullRequests'; -import { isAuthorizationError } from '../../utils'; const columns: TableColumn[] = [ { @@ -106,15 +104,6 @@ export const PullRequestTable = ({ defaultLimit }: PullRequestTableProps) => { ); if (error) { - if (isAuthorizationError(error)) { - return ( - - ); - } return ; } diff --git a/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx b/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx index 9561c76a2c..c5da207bd6 100644 --- a/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx +++ b/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx @@ -16,7 +16,6 @@ import { Content, - EmptyState, Header, Page, Progress, @@ -29,7 +28,6 @@ import { FilterType } from './lib/filters'; import { PullRequestGrid } from './lib/PullRequestGrid'; import { useDashboardPullRequests } from '../../hooks'; import { useFilterProcessor } from './lib/hooks'; -import { isAuthorizationError } from '../../utils'; type PullRequestsPageContentProps = { pullRequestGroups: PullRequestGroup[] | undefined; @@ -47,15 +45,6 @@ const PullRequestsPageContent = ({ } if (error) { - if (isAuthorizationError(error)) { - return ( - - ); - } return ; } diff --git a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx index a45c6fe831..6c9c427f9c 100644 --- a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx +++ b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx @@ -28,7 +28,9 @@ import { useEntity } from '@backstage/plugin-catalog-react'; import React from 'react'; import { useReadme } from '../../hooks'; -import { isAuthorizationError } from '../../utils'; +import { RequirePermission } from '@backstage/plugin-permission-react'; +import { azureDevOpsReadmeReadPermission } from '@backstage/plugin-azure-devops-common'; +import { stringifyEntityRef } from '@backstage/catalog-model'; const useStyles = makeStyles(theme => ({ readMe: { @@ -64,15 +66,7 @@ function isNotFoundError(error: any): boolean { } const ReadmeCardError = ({ error }: ErrorProps) => { - if (isAuthorizationError(error)) { - return ( - - ); - } else if (isNotFoundError(error)) { + if (isNotFoundError(error)) { return ( { } return ( - + } > - - - - + + + + + + ); }; diff --git a/plugins/azure-devops/src/utils/index.ts b/plugins/azure-devops/src/utils/index.ts index a05ea4f650..998bb0e23d 100644 --- a/plugins/azure-devops/src/utils/index.ts +++ b/plugins/azure-devops/src/utils/index.ts @@ -18,4 +18,3 @@ export * from './arrayHas'; export * from './equalsIgnoreCase'; export * from './getDurationFromDates'; export * from './getAnnotationValuesFromEntity'; -export * from './validateError'; diff --git a/plugins/azure-devops/src/utils/validateError.ts b/plugins/azure-devops/src/utils/validateError.ts deleted file mode 100644 index fca1687013..0000000000 --- a/plugins/azure-devops/src/utils/validateError.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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. - */ -export function isAuthorizationError(error: any): boolean { - return error?.response?.status === 403; -} diff --git a/yarn.lock b/yarn.lock index d0b27cb311..e7338a85fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5164,6 +5164,7 @@ __metadata: "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-azure-devops-common": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 From bfd63c99f6d1efe5b091ef52ac76b69ef6e4072d Mon Sep 17 00:00:00 2001 From: Deepankumar Loganathan Date: Sat, 3 Feb 2024 18:31:59 +0100 Subject: [PATCH 07/12] README updated and comments fixed Signed-off-by: Deepankumar Loganathan --- .../src/service/router.ts | 8 +-- plugins/azure-devops/README.md | 59 +++++++++++++++++++ .../EntityPageAzureGitTags.tsx | 9 +-- .../EntityPageAzurePipelines.tsx | 9 +-- .../EntityPageAzurePullRequests.tsx | 9 +-- .../src/components/ReadmeCard/ReadmeCard.tsx | 8 +-- 6 files changed, 67 insertions(+), 35 deletions(-) diff --git a/plugins/azure-devops-backend/src/service/router.ts b/plugins/azure-devops-backend/src/service/router.ts index a77aa42c8a..b9667a9ade 100644 --- a/plugins/azure-devops-backend/src/service/router.ts +++ b/plugins/azure-devops-backend/src/service/router.ts @@ -131,7 +131,7 @@ export async function createRouter( const host = req.query.host?.toString(); const org = req.query.org?.toString(); - const entityRef = req.query.entityRef?.toString(); + const entityRef = req.query.entityRef; if (typeof entityRef !== 'string') { throw new InputError('Invalid entityRef, not a string'); } @@ -181,7 +181,7 @@ export async function createRouter( status: status, }; - const entityRef = req.query.entityRef?.toString(); + const entityRef = req.query.entityRef; if (typeof entityRef !== 'string') { throw new InputError('Invalid entityRef, not a string'); } @@ -290,7 +290,7 @@ export async function createRouter( const host = req.query.host?.toString(); const org = req.query.org?.toString(); - const entityRef = req.query.entityRef?.toString(); + const entityRef = req.query.entityRef; if (typeof entityRef !== 'string') { throw new InputError('Invalid entityRef, not a string'); } @@ -355,7 +355,7 @@ export async function createRouter( const { projectName, repoName } = req.params; - const entityRef = req.query.entityRef?.toString(); + const entityRef = req.query.entityRef; if (typeof entityRef !== 'string') { throw new InputError('Invalid entityRef, not a string'); } diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md index 935e78c82c..996ff57690 100644 --- a/plugins/azure-devops/README.md +++ b/plugins/azure-devops/README.md @@ -319,3 +319,62 @@ To get the README component working you'll need to do the following two steps: - You'll need to add the `EntitySwitch.Case` above from step 2 to all the entity sections you want to see Readme in. For example if you wanted to see Readme when looking at Website entities then you would need to add this to the `websiteEntityPage` section. - The `if` prop is optional on the `EntitySwitch.Case`, you can remove it if you always want to see the tab even if the entity being viewed does not have the needed annotation - The `maxHeight` property on the `EntityAzureReadmeCard` will set the maximum screen size you would like to see, if not set it will default to 100% + +## Permission Framework + +Azure DevOps plugin is now supporting the permission framework for PRs, GitTags, Pipelines & ReadMe. + +To enable the permission on the legacy backend system add the below config in `packages/backend/src/plugins/azure-devops.ts`. + +```diff +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return createRouter({ + logger: env.logger, + config: env.config, + reader: env.reader, ++ permissions: env.permissions, + }); +} +``` + +### Configure Permission + +To apply the permission rule add the following in `packages/backend/src/plugins/permissions.ts`. + +> Note this is example only `azureDevOpsPullRequestDashboardReadPermission` is Basic permission rest all Resource Permission. As an adopter you can configure how you wanted. + +```diff ++ import { ++ azureDevOpsPullRequestReadPermission, ++ azureDevOpsPipelineReadPermission, ++ azureDevOpsGitTagReadPermission, ++ azureDevOpsReadmeReadPermission, ++ azureDevOpsPullRequestDashboardReadPermission } from '@backstage/azure-devops-common'; +... +async handle( + request: PolicyQuery, + user?: BackstageIdentityResponse, +): Promise { ++ if ( isPermission(request.permission, azureDevOpsPullRequestReadPermission) || ++ isPermission(request.permission, azureDevOpsPipelineReadPermission) || ++ isPermission(request.permission, azureDevOpsGitTagReadPermission) || ++ isPermission(request.permission, azureDevOpsReadmeReadPermission)) { ++ return createTodoListConditionalDecision( ++ request.permission, ++ catalogConditions.isEntityOwner({ ++ claims: user?.identity.ownershipEntityRefs ?? [], ++ }), ++ ); ++ } + ++ if ( isPermission(request.permission, azureDevOpsPullRequestDashboardReadPermission) { ++ result: AuthorizeResult.ALLOW, ++ } + + return { + result: AuthorizeResult.ALLOW, + }; +} +``` diff --git a/plugins/azure-devops/src/components/EntityPageAzureGitTags/EntityPageAzureGitTags.tsx b/plugins/azure-devops/src/components/EntityPageAzureGitTags/EntityPageAzureGitTags.tsx index 1632649d59..3e174fd31b 100644 --- a/plugins/azure-devops/src/components/EntityPageAzureGitTags/EntityPageAzureGitTags.tsx +++ b/plugins/azure-devops/src/components/EntityPageAzureGitTags/EntityPageAzureGitTags.tsx @@ -20,7 +20,6 @@ import React from 'react'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { useEntity } from '@backstage/plugin-catalog-react'; import { RequirePermission } from '@backstage/plugin-permission-react'; -import { EmptyState } from '@backstage/core-components'; export const EntityPageAzureGitTags = () => { const { entity } = useEntity(); @@ -28,13 +27,7 @@ export const EntityPageAzureGitTags = () => { - } + errorPage={null} > diff --git a/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx b/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx index 39e3da1a7e..d8c6483a3e 100644 --- a/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx +++ b/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx @@ -21,7 +21,6 @@ import { useEntity } from '@backstage/plugin-catalog-react'; import { azureDevOpsPipelineReadPermission } from '@backstage/plugin-azure-devops-common'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { RequirePermission } from '@backstage/plugin-permission-react'; -import { EmptyState } from '@backstage/core-components'; export const EntityPageAzurePipelines = (props: { defaultLimit?: number }) => { const { entity } = useEntity(); @@ -32,13 +31,7 @@ export const EntityPageAzurePipelines = (props: { defaultLimit?: number }) => { - } + errorPage={null} > diff --git a/plugins/azure-devops/src/components/EntityPageAzurePullRequests/EntityPageAzurePullRequests.tsx b/plugins/azure-devops/src/components/EntityPageAzurePullRequests/EntityPageAzurePullRequests.tsx index 7b828c512c..1d59ea4b8f 100644 --- a/plugins/azure-devops/src/components/EntityPageAzurePullRequests/EntityPageAzurePullRequests.tsx +++ b/plugins/azure-devops/src/components/EntityPageAzurePullRequests/EntityPageAzurePullRequests.tsx @@ -20,7 +20,6 @@ import React from 'react'; import { RequirePermission } from '@backstage/plugin-permission-react'; import { useEntity } from '@backstage/plugin-catalog-react'; import { stringifyEntityRef } from '@backstage/catalog-model'; -import { EmptyState } from '@backstage/core-components'; export const EntityPageAzurePullRequests = (props: { defaultLimit?: number; @@ -30,13 +29,7 @@ export const EntityPageAzurePullRequests = (props: { - } + errorPage={null} > diff --git a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx index 6c9c427f9c..b2c874a3ba 100644 --- a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx +++ b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx @@ -103,13 +103,7 @@ export const ReadmeCard = (props: Props) => { - } + errorPage={null} > Date: Sat, 10 Feb 2024 10:25:34 +0100 Subject: [PATCH 08/12] minor fix & documentation updated Signed-off-by: Deepankumar Loganathan --- .changeset/ten-dryers-compete.md | 2 +- plugins/azure-devops/README.md | 17 +++++++++++------ .../azure-devops/src/api/AzureDevOpsClient.ts | 1 + 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.changeset/ten-dryers-compete.md b/.changeset/ten-dryers-compete.md index 02715d0736..f1591c0040 100644 --- a/.changeset/ten-dryers-compete.md +++ b/.changeset/ten-dryers-compete.md @@ -4,4 +4,4 @@ '@backstage/plugin-azure-devops-common': patch --- -Azure DevOps plugin is now integrated with permission framework for its core features, see the https://github.com/backstage/backstage/blob/master/plugins/azure-devops/README.md for more details. +Azure DevOps plugin is now integrated with permission framework for its core features, see the https://github.com/backstage/backstage/blob/master/plugins/azure-devops/README.md#permission-framework for more details. diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md index 996ff57690..9e5ec384f8 100644 --- a/plugins/azure-devops/README.md +++ b/plugins/azure-devops/README.md @@ -322,9 +322,11 @@ To get the README component working you'll need to do the following two steps: ## Permission Framework -Azure DevOps plugin is now supporting the permission framework for PRs, GitTags, Pipelines & ReadMe. +Azure DevOps plugin supports the permission framework for PRs, GitTags, Pipelines and Readme features. -To enable the permission on the legacy backend system add the below config in `packages/backend/src/plugins/azure-devops.ts`. +New Backend you can skip the below and proceed with [permission configuration](#configure-permission) + +To enable permissions for the legacy backend system in `packages/backend/src/plugins/azure-devops.ts` add the following. ```diff export default async function createPlugin( @@ -341,17 +343,18 @@ export default async function createPlugin( ### Configure Permission -To apply the permission rule add the following in `packages/backend/src/plugins/permissions.ts`. +To apply the permission rules add the following in `packages/backend/src/plugins/permissions.ts`. -> Note this is example only `azureDevOpsPullRequestDashboardReadPermission` is Basic permission rest all Resource Permission. As an adopter you can configure how you wanted. +> Note: the following is just an example of how you might want to setup permissions, as an Adopter you can configure this to fit your needs. Also all the permissions are Resource Permissions as they work with an Entity with the exception of `azureDevOpsPullRequestDashboardReadPermission`. ```diff + + import { + azureDevOpsPullRequestReadPermission, + azureDevOpsPipelineReadPermission, + azureDevOpsGitTagReadPermission, + azureDevOpsReadmeReadPermission, -+ azureDevOpsPullRequestDashboardReadPermission } from '@backstage/azure-devops-common'; ++ azureDevOpsPullRequestDashboardReadPermission } from '@backstage/plugin-azure-devops-common'; ... async handle( request: PolicyQuery, @@ -361,7 +364,7 @@ async handle( + isPermission(request.permission, azureDevOpsPipelineReadPermission) || + isPermission(request.permission, azureDevOpsGitTagReadPermission) || + isPermission(request.permission, azureDevOpsReadmeReadPermission)) { -+ return createTodoListConditionalDecision( ++ return createCatalogConditionalDecision( + request.permission, + catalogConditions.isEntityOwner({ + claims: user?.identity.ownershipEntityRefs ?? [], @@ -370,7 +373,9 @@ async handle( + } + if ( isPermission(request.permission, azureDevOpsPullRequestDashboardReadPermission) { ++ return { + result: AuthorizeResult.ALLOW, ++ }; + } return { diff --git a/plugins/azure-devops/src/api/AzureDevOpsClient.ts b/plugins/azure-devops/src/api/AzureDevOpsClient.ts index e1fde33555..ea78f3d3a1 100644 --- a/plugins/azure-devops/src/api/AzureDevOpsClient.ts +++ b/plugins/azure-devops/src/api/AzureDevOpsClient.ts @@ -198,6 +198,7 @@ export class AzureDevOpsClient implements AzureDevOpsApi { if (opts.path) { queryString.append('path', opts.path); } + queryString.append('entityRef', opts.entityRef); return await this.get( `readme/${encodeURIComponent(opts.project)}/${encodeURIComponent( opts.repo, From 168bae03d7c3a0f93cc53cc59788a50f1aafcde6 Mon Sep 17 00:00:00 2001 From: Deepankumar Loganathan Date: Thu, 15 Feb 2024 18:57:47 +0100 Subject: [PATCH 09/12] fixed ReadmeCard & documentation updated Signed-off-by: Deepankumar Loganathan --- plugins/azure-devops/README.md | 14 +++++++++ .../src/components/ReadmeCard/ReadmeCard.tsx | 30 +++++++------------ 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md index 9e5ec384f8..939330812e 100644 --- a/plugins/azure-devops/README.md +++ b/plugins/azure-devops/README.md @@ -324,6 +324,11 @@ To get the README component working you'll need to do the following two steps: Azure DevOps plugin supports the permission framework for PRs, GitTags, Pipelines and Readme features. +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-azure-devops-common +``` + New Backend you can skip the below and proceed with [permission configuration](#configure-permission) To enable permissions for the legacy backend system in `packages/backend/src/plugins/azure-devops.ts` add the following. @@ -355,6 +360,15 @@ To apply the permission rules add the following in `packages/backend/src/plugins + azureDevOpsGitTagReadPermission, + azureDevOpsReadmeReadPermission, + azureDevOpsPullRequestDashboardReadPermission } from '@backstage/plugin-azure-devops-common'; ++ import { ++ AuthorizeResult, ++ PolicyDecision, ++ isPermission, ++ } from '@backstage/plugin-permission-common'; ++ import { ++ catalogConditions, ++ createCatalogConditionalDecision, ++ } from '@backstage/plugin-catalog-backend/alpha'; ... async handle( request: PolicyQuery, diff --git a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx index b2c874a3ba..5443d9232b 100644 --- a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx +++ b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx @@ -28,9 +28,6 @@ import { useEntity } from '@backstage/plugin-catalog-react'; import React from 'react'; import { useReadme } from '../../hooks'; -import { RequirePermission } from '@backstage/plugin-permission-react'; -import { azureDevOpsReadmeReadPermission } from '@backstage/plugin-azure-devops-common'; -import { stringifyEntityRef } from '@backstage/catalog-model'; const useStyles = makeStyles(theme => ({ readMe: { @@ -90,7 +87,6 @@ const ReadmeCardError = ({ error }: ErrorProps) => { export const ReadmeCard = (props: Props) => { const classes = useStyles(); const { entity } = useEntity(); - const { loading, error, item: value } = useReadme(entity); if (loading) { @@ -100,22 +96,16 @@ export const ReadmeCard = (props: Props) => { } return ( - - - - - - - + + + + ); }; From 75a28c79d856c18ed8e642d68af81d06a869196d Mon Sep 17 00:00:00 2001 From: Deepankumar Loganathan Date: Fri, 16 Feb 2024 14:32:21 +0100 Subject: [PATCH 10/12] PR dashboard permission docs updated Signed-off-by: Deepankumar Loganathan --- plugins/azure-devops/README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md index 939330812e..15f2606542 100644 --- a/plugins/azure-devops/README.md +++ b/plugins/azure-devops/README.md @@ -397,3 +397,31 @@ async handle( }; } ``` + +#### Configure Azure DevOps PullRequest Dashboard Permission + +To Integrate Azure DevOps PullRequest Dashboard permssion, add the following in `packages/app/src/App.tsx` + +```diff +... ++ import { RequirePermission } from '@backstage/plugin-permission-react'; ++ import { azureDevOpsPullRequestDashboardReadPermission } from '@backstage/plugin-azure-devops-common'; +... +... + const routes =( + + ... ++ ++ ++ ++ } ++ > ++ + ... + + ) +... +``` From b65ee04506890ce172553754d365c0c56f615d7e Mon Sep 17 00:00:00 2001 From: Deepankumar Loganathan Date: Fri, 16 Feb 2024 14:57:21 +0100 Subject: [PATCH 11/12] added RequirePermission in PullRequestPage Signed-off-by: Deepankumar Loganathan --- plugins/azure-devops/README.md | 28 ------------------- .../PullRequestsPage/PullRequestsPage.tsx | 16 +++++++---- 2 files changed, 11 insertions(+), 33 deletions(-) diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md index 15f2606542..939330812e 100644 --- a/plugins/azure-devops/README.md +++ b/plugins/azure-devops/README.md @@ -397,31 +397,3 @@ async handle( }; } ``` - -#### Configure Azure DevOps PullRequest Dashboard Permission - -To Integrate Azure DevOps PullRequest Dashboard permssion, add the following in `packages/app/src/App.tsx` - -```diff -... -+ import { RequirePermission } from '@backstage/plugin-permission-react'; -+ import { azureDevOpsPullRequestDashboardReadPermission } from '@backstage/plugin-azure-devops-common'; -... -... - const routes =( - - ... -+ -+ -+ -+ } -+ > -+ - ... - - ) -... -``` diff --git a/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx b/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx index c5da207bd6..5971a03afd 100644 --- a/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx +++ b/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx @@ -28,6 +28,8 @@ import { FilterType } from './lib/filters'; import { PullRequestGrid } from './lib/PullRequestGrid'; import { useDashboardPullRequests } from '../../hooks'; import { useFilterProcessor } from './lib/hooks'; +import { RequirePermission } from '@backstage/plugin-permission-react'; +import { azureDevOpsPullRequestDashboardReadPermission } from '@backstage/plugin-azure-devops-common'; type PullRequestsPageContentProps = { pullRequestGroups: PullRequestGroup[] | undefined; @@ -98,11 +100,15 @@ export const PullRequestsPage = (props: PullRequestsPageProps) => {
- + + + ); From 7b4822e48f0e5c6f9ffc87c36a8dc94bd56a2ed8 Mon Sep 17 00:00:00 2001 From: Deepankumar Loganathan Date: Wed, 21 Feb 2024 20:49:45 +0100 Subject: [PATCH 12/12] removed duplicate & fixed test cases Signed-off-by: Deepankumar Loganathan --- .../src/service/router.test.ts | 18 ++++++++++++------ .../azure-devops-backend/src/service/router.ts | 1 - 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index 9f874923fd..d1e9179dc1 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -559,10 +559,13 @@ describe('createRouter', () => { content, url, }); + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.ALLOW }, + ]); - const response = await request(app).get( - '/readme/myProject/myRepo?path=README_NOT_DEFAULT.md', - ); + const response = await request(app) + .get('/readme/myProject/myRepo?path=README_NOT_DEFAULT.md') + .query({ entityRef: 'component:default/mycomponent' }); expect(azureDevOpsApi.getReadme).toHaveBeenCalledWith( 'host.com', 'myOrg', @@ -587,10 +590,13 @@ describe('createRouter', () => { content, url, }); + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.ALLOW }, + ]); - const response = await request(app).get( - '/readme/myProject/myRepo?path=/my-path/README.md', - ); + const response = await request(app) + .get('/readme/myProject/myRepo?path=/my-path/README.md') + .query({ entityRef: 'component:default/mycomponent' }); expect(azureDevOpsApi.getReadme).toHaveBeenCalledWith( 'host.com', 'myOrg', diff --git a/plugins/azure-devops-backend/src/service/router.ts b/plugins/azure-devops-backend/src/service/router.ts index b9667a9ade..239de78fbe 100644 --- a/plugins/azure-devops-backend/src/service/router.ts +++ b/plugins/azure-devops-backend/src/service/router.ts @@ -26,7 +26,6 @@ import { Logger } from 'winston'; import { PullRequestsDashboardProvider } from '../api/PullRequestsDashboardProvider'; import Router from 'express-promise-router'; import { errorHandler, UrlReader } from '@backstage/backend-common'; -import { InputError } from '@backstage/errors'; import express from 'express'; import { InputError, NotAllowedError } from '@backstage/errors'; import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';