permissio framework add to azure-devops-backend
Signed-off-by: Deepankumar Loganathan <deepan0433@gmail.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user