permissio framework add to azure-devops-backend

Signed-off-by: Deepankumar Loganathan <deepan0433@gmail.com>
This commit is contained in:
Deepankumar Loganathan
2024-01-29 07:27:27 +01:00
parent dc99b56bc8
commit a35225da19
4 changed files with 202 additions and 3 deletions
@@ -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",
@@ -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<AzureDevOpsApi>;
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',
@@ -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<express.Router> {
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,
+5
View File
@@ -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