Tech Insights permissions coverage (#3867)
* Tech Insights permissions coverage Signed-off-by: João Martins <joao@roadie.io> * chore: trigger CI Signed-off-by: João Martins <joao@roadie.io> * missing dependencies in router Signed-off-by: João Martins <joao@roadie.io> * add api reports Signed-off-by: João Martins <joao@roadie.io> --------- Signed-off-by: João Martins <joao@roadie.io>
This commit is contained in:
parent
d8b448d4d8
commit
79a9ab9ec6
|
|
@ -2,6 +2,19 @@
|
|||
|
||||
The backend plugin for Tech Insights.
|
||||
|
||||
## Breaking Changes in v3.0.0
|
||||
|
||||
This version includes breaking changes related to permissions. Please review the changes carefully before upgrading:
|
||||
|
||||
- Added required permissions for accessing Tech Insights features
|
||||
- Users must now have the appropriate policies added to their roles to access Tech Insights functionality
|
||||
- This change enforces better security by ensuring explicit permission grants for Tech Insights operations
|
||||
|
||||
To upgrade, you'll need to:
|
||||
|
||||
1. Update your permission policies to include the necessary Tech Insights permissions
|
||||
2. Ensure your users' roles have the appropriate policies assigned
|
||||
|
||||
It provides the API for the frontend tech insights, scorecards and fact visualization functionality, as well as a framework to run fact retrievers and store fact values in to a data store.
|
||||
|
||||
Looking for the old backend installation docs? Visit [here](./docs/old-backend-system.md).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@backstage-community/plugin-tech-insights-backend",
|
||||
"version": "2.2.1",
|
||||
"version": "3.0.0",
|
||||
"backstage": {
|
||||
"role": "backend-plugin",
|
||||
"pluginId": "tech-insights",
|
||||
|
|
@ -57,6 +57,8 @@
|
|||
"@backstage/catalog-model": "^1.7.3",
|
||||
"@backstage/config": "^1.3.2",
|
||||
"@backstage/errors": "^1.2.7",
|
||||
"@backstage/plugin-permission-common": "^0.8.4",
|
||||
"@backstage/plugin-permission-node": "^0.8.7",
|
||||
"@backstage/types": "^1.2.1",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/luxon": "^3.0.0",
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ import { FactLifecycle } from '@backstage-community/plugin-tech-insights-node';
|
|||
import { FactRetriever } from '@backstage-community/plugin-tech-insights-node';
|
||||
import { FactRetrieverRegistration } from '@backstage-community/plugin-tech-insights-node';
|
||||
import { FactRetrieverRegistry as FactRetrieverRegistry_2 } from '@backstage-community/plugin-tech-insights-node';
|
||||
import { HttpAuthService } from '@backstage/backend-plugin-api';
|
||||
import { HumanDuration } from '@backstage/types';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { PermissionsService } from '@backstage/backend-plugin-api';
|
||||
import { PersistenceContext as PersistenceContext_2 } from '@backstage-community/plugin-tech-insights-node';
|
||||
import { SchedulerService } from '@backstage/backend-plugin-api';
|
||||
import { UrlReaderService } from '@backstage/backend-plugin-api';
|
||||
|
|
@ -90,7 +92,9 @@ export interface RouterOptions<
|
|||
> {
|
||||
config: Config;
|
||||
factChecker?: FactChecker<CheckType, CheckResultType>;
|
||||
httpAuth: HttpAuthService;
|
||||
logger: LoggerService;
|
||||
permissions: PermissionsService;
|
||||
persistenceContext: PersistenceContext_2;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,8 @@ export const techInsightsPlugin = createBackendPlugin({
|
|||
scheduler: coreServices.scheduler,
|
||||
auth: coreServices.auth,
|
||||
urlReader: coreServices.urlReader,
|
||||
httpAuth: coreServices.httpAuth,
|
||||
permissions: coreServices.permissions,
|
||||
},
|
||||
async init({
|
||||
config,
|
||||
|
|
@ -113,6 +115,8 @@ export const techInsightsPlugin = createBackendPlugin({
|
|||
scheduler,
|
||||
auth,
|
||||
urlReader,
|
||||
httpAuth,
|
||||
permissions,
|
||||
}) {
|
||||
const factRetrievers: FactRetrieverRegistration[] = Object.entries(
|
||||
addedFactRetrievers,
|
||||
|
|
@ -145,6 +149,8 @@ export const techInsightsPlugin = createBackendPlugin({
|
|||
...context,
|
||||
config,
|
||||
logger,
|
||||
permissions,
|
||||
httpAuth,
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,10 +23,53 @@ import {
|
|||
PersistenceContext,
|
||||
TechInsightsStore,
|
||||
} from '@backstage-community/plugin-tech-insights-node';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import { DateTime } from 'luxon';
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
import { DefaultSchedulerService } from '@backstage/backend-defaults/scheduler';
|
||||
|
||||
const setupRouter = async (
|
||||
mockPersistenceContext: PersistenceContext,
|
||||
allow: boolean,
|
||||
) => {
|
||||
const database = mockServices.database.mock({
|
||||
migrations: { skip: true },
|
||||
});
|
||||
const logger = mockServices.logger.mock();
|
||||
const urlReader = mockServices.urlReader.mock();
|
||||
const techInsightsContext = await buildTechInsightsContext({
|
||||
database,
|
||||
logger,
|
||||
factRetrievers: [],
|
||||
scheduler: DefaultSchedulerService.create({ database, logger }),
|
||||
config: ConfigReader.fromConfigs([]),
|
||||
discovery: {
|
||||
getBaseUrl: (_: string) => Promise.resolve('http://mock.url'),
|
||||
getExternalBaseUrl: (_: string) => Promise.resolve('http://mock.url'),
|
||||
},
|
||||
auth: mockServices.auth(),
|
||||
urlReader,
|
||||
});
|
||||
|
||||
const router = await createRouter({
|
||||
logger,
|
||||
config: ConfigReader.fromConfigs([]),
|
||||
...techInsightsContext,
|
||||
persistenceContext: mockPersistenceContext,
|
||||
permissions: mockServices.permissions.mock({
|
||||
authorize: async () => [
|
||||
{ result: allow ? AuthorizeResult.ALLOW : AuthorizeResult.DENY },
|
||||
],
|
||||
authorizeConditional: async () => [
|
||||
{ result: allow ? AuthorizeResult.ALLOW : AuthorizeResult.DENY },
|
||||
],
|
||||
}),
|
||||
httpAuth: mockServices.httpAuth(),
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
describe('Tech Insights router tests', () => {
|
||||
let app: express.Express;
|
||||
|
||||
|
|
@ -46,32 +89,8 @@ describe('Tech Insights router tests', () => {
|
|||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
const database = mockServices.database.mock({
|
||||
migrations: { skip: true },
|
||||
});
|
||||
const logger = mockServices.logger.mock();
|
||||
const urlReader = mockServices.urlReader.mock();
|
||||
const techInsightsContext = await buildTechInsightsContext({
|
||||
database,
|
||||
logger,
|
||||
factRetrievers: [],
|
||||
scheduler: DefaultSchedulerService.create({ database, logger }),
|
||||
config: ConfigReader.fromConfigs([]),
|
||||
discovery: {
|
||||
getBaseUrl: (_: string) => Promise.resolve('http://mock.url'),
|
||||
getExternalBaseUrl: (_: string) => Promise.resolve('http://mock.url'),
|
||||
},
|
||||
auth: mockServices.auth(),
|
||||
urlReader,
|
||||
});
|
||||
|
||||
const router = await createRouter({
|
||||
logger,
|
||||
config: ConfigReader.fromConfigs([]),
|
||||
...techInsightsContext,
|
||||
persistenceContext: mockPersistenceContext,
|
||||
});
|
||||
beforeEach(async () => {
|
||||
const router = await setupRouter(mockPersistenceContext, true);
|
||||
|
||||
app = express().use(router);
|
||||
});
|
||||
|
|
@ -80,6 +99,12 @@ describe('Tech Insights router tests', () => {
|
|||
await request(app).get('/fact-schemas').expect(200);
|
||||
expect(latestSchemasMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not allow access when not authorized', async () => {
|
||||
const router = await setupRouter(mockPersistenceContext, false);
|
||||
app = express().use(router);
|
||||
await request(app).get('/fact-schemas').expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/checks', () => {
|
||||
|
|
@ -113,6 +138,20 @@ describe('Tech Insights router tests', () => {
|
|||
.expect(200);
|
||||
expect(latestFactsByIdsMock).toHaveBeenCalledWith(['secondId'], 'a:a/a');
|
||||
});
|
||||
|
||||
it('should not allow access when not authorized', async () => {
|
||||
const router = await setupRouter(mockPersistenceContext, false);
|
||||
|
||||
app = express().use(router);
|
||||
|
||||
await request(app)
|
||||
.get('/facts/latest')
|
||||
.query({
|
||||
entity: 'a:a/a',
|
||||
ids: ['firstId', 'secondId'],
|
||||
})
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/facts/range', () => {
|
||||
|
|
@ -164,5 +203,13 @@ describe('Tech Insights router tests', () => {
|
|||
DateTime.fromISO('2022-11-11T11:11:11.000+00:00'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not allow access when not authorized', async () => {
|
||||
const router = await setupRouter(mockPersistenceContext, false);
|
||||
|
||||
app = express().use(router);
|
||||
|
||||
await request(app).get('/facts/range').expect(403);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import express, { Request } from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
|
|
@ -25,6 +25,10 @@ import {
|
|||
import {
|
||||
CheckResult,
|
||||
Check,
|
||||
techInsightsCheckReadPermission,
|
||||
techInsightsCheckUpdatePermission,
|
||||
techInsightsPermissions,
|
||||
techInsightsFactRetrieverReadPermission,
|
||||
} from '@backstage-community/plugin-tech-insights-common';
|
||||
import { DateTime } from 'luxon';
|
||||
import {
|
||||
|
|
@ -33,8 +37,18 @@ import {
|
|||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { serializeError } from '@backstage/errors';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
LoggerService,
|
||||
PermissionsService,
|
||||
HttpAuthService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
BasicPermission,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
|
||||
import { NotAllowedError } from '@backstage/errors';
|
||||
import pLimit from 'p-limit';
|
||||
|
||||
/**
|
||||
|
|
@ -67,6 +81,16 @@ export interface RouterOptions<
|
|||
* Implementation of Winston logger
|
||||
*/
|
||||
logger: LoggerService;
|
||||
|
||||
/**
|
||||
* Implementation of PermissionsService
|
||||
*/
|
||||
permissions: PermissionsService;
|
||||
|
||||
/**
|
||||
* Implementation of HttpAuthService
|
||||
*/
|
||||
httpAuth: HttpAuthService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -84,19 +108,54 @@ export async function createRouter<
|
|||
CheckResultType extends CheckResult,
|
||||
>(options: RouterOptions<CheckType, CheckResultType>): Promise<express.Router> {
|
||||
const router = Router();
|
||||
|
||||
const permissionsIntegrationRouter = createPermissionIntegrationRouter({
|
||||
permissions: techInsightsPermissions,
|
||||
});
|
||||
|
||||
router.use(express.json());
|
||||
const { persistenceContext, factChecker, logger, config } = options;
|
||||
router.use(permissionsIntegrationRouter);
|
||||
|
||||
const {
|
||||
persistenceContext,
|
||||
factChecker,
|
||||
logger,
|
||||
config,
|
||||
permissions,
|
||||
httpAuth,
|
||||
} = options;
|
||||
const { techInsightsStore } = persistenceContext;
|
||||
|
||||
const factory = MiddlewareFactory.create({ logger, config });
|
||||
|
||||
const authorize = async (request: Request, permission: BasicPermission) => {
|
||||
const decision = (
|
||||
await permissions.authorize([{ permission: permission }], {
|
||||
credentials: await httpAuth.credentials(request),
|
||||
})
|
||||
)[0];
|
||||
|
||||
return decision;
|
||||
};
|
||||
|
||||
if (factChecker) {
|
||||
logger.info('Fact checker configured. Enabling fact checking endpoints.');
|
||||
router.get('/checks', async (_req, res) => {
|
||||
router.get('/checks', async (req, res) => {
|
||||
const decision = await authorize(req, techInsightsCheckReadPermission);
|
||||
|
||||
if (decision.result === AuthorizeResult.DENY) {
|
||||
throw new NotAllowedError('Unauthorized');
|
||||
}
|
||||
return res.json(await factChecker.getChecks());
|
||||
});
|
||||
|
||||
router.post('/checks/run/:namespace/:kind/:name', async (req, res) => {
|
||||
const decision = await authorize(req, techInsightsCheckUpdatePermission);
|
||||
|
||||
if (decision.result === AuthorizeResult.DENY) {
|
||||
throw new NotAllowedError('Unauthorized');
|
||||
}
|
||||
|
||||
const { namespace, kind, name } = req.params;
|
||||
const { checks }: { checks: string[] } = req.body;
|
||||
const entityTriplet = stringifyEntityRef({ namespace, kind, name });
|
||||
|
|
@ -107,6 +166,11 @@ export async function createRouter<
|
|||
const checksRunConcurrency =
|
||||
config.getOptionalNumber('techInsights.checksRunConcurrency') || 100;
|
||||
router.post('/checks/run', async (req, res) => {
|
||||
const decision = await authorize(req, techInsightsCheckUpdatePermission);
|
||||
|
||||
if (decision.result === AuthorizeResult.DENY) {
|
||||
throw new NotAllowedError('Unauthorized');
|
||||
}
|
||||
const checks: string[] = req.body.checks;
|
||||
let entities: CompoundEntityRef[] = req.body.entities;
|
||||
if (entities.length === 0) {
|
||||
|
|
@ -144,6 +208,15 @@ export async function createRouter<
|
|||
}
|
||||
|
||||
router.get('/fact-schemas', async (req, res) => {
|
||||
const decision = await authorize(
|
||||
req,
|
||||
techInsightsFactRetrieverReadPermission,
|
||||
);
|
||||
|
||||
if (decision.result === AuthorizeResult.DENY) {
|
||||
throw new NotAllowedError('Unauthorized');
|
||||
}
|
||||
|
||||
const ids = req.query.ids as string[];
|
||||
return res.json(await techInsightsStore.getLatestSchemas(ids));
|
||||
});
|
||||
|
|
@ -152,6 +225,14 @@ export async function createRouter<
|
|||
* /facts/latest?entity=component:default/mycomponent&ids[]=factRetrieverId1&ids[]=factRetrieverId2
|
||||
*/
|
||||
router.get('/facts/latest', async (req, res) => {
|
||||
const decision = await authorize(
|
||||
req,
|
||||
techInsightsFactRetrieverReadPermission,
|
||||
);
|
||||
|
||||
if (decision.result === AuthorizeResult.DENY) {
|
||||
throw new NotAllowedError('Unauthorized');
|
||||
}
|
||||
const { entity } = req.query;
|
||||
const { namespace, kind, name } = parseEntityRef(entity as string);
|
||||
|
||||
|
|
@ -173,6 +254,15 @@ export async function createRouter<
|
|||
* /facts/range?entity=component:default/mycomponent&startDateTime=2021-12-24T01:23:45&endDateTime=2021-12-31T23:59:59&ids[]=factRetrieverId1&ids[]=factRetrieverId2
|
||||
*/
|
||||
router.get('/facts/range', async (req, res) => {
|
||||
const decision = await authorize(
|
||||
req,
|
||||
techInsightsFactRetrieverReadPermission,
|
||||
);
|
||||
|
||||
if (decision.result === AuthorizeResult.DENY) {
|
||||
throw new NotAllowedError('Unauthorized');
|
||||
}
|
||||
|
||||
const { entity } = req.query;
|
||||
const { namespace, kind, name } = parseEntityRef(entity as string);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
> 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 { DateTime } from 'luxon';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
|
||||
|
|
@ -93,5 +94,17 @@ export interface InsightFacts {
|
|||
};
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const techInsightsCheckReadPermission: BasicPermission;
|
||||
|
||||
// @public (undocumented)
|
||||
export const techInsightsCheckUpdatePermission: BasicPermission;
|
||||
|
||||
// @public (undocumented)
|
||||
export const techInsightsFactRetrieverReadPermission: BasicPermission;
|
||||
|
||||
// @public (undocumented)
|
||||
export const techInsightsPermissions: BasicPermission[];
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -15,3 +15,4 @@
|
|||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './permissions';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const techInsightsCheckReadPermission = createPermission({
|
||||
name: 'tech-insights.check.read',
|
||||
attributes: {
|
||||
action: 'read',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const techInsightsCheckUpdatePermission = createPermission({
|
||||
name: 'tech-insights.check.run',
|
||||
attributes: {
|
||||
action: 'update',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const techInsightsFactRetrieverReadPermission = createPermission({
|
||||
name: 'tech-insights.fact-retriever.read',
|
||||
attributes: {
|
||||
action: 'read',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const techInsightsPermissions = [
|
||||
techInsightsCheckReadPermission,
|
||||
techInsightsCheckUpdatePermission,
|
||||
techInsightsFactRetrieverReadPermission,
|
||||
];
|
||||
|
|
@ -2778,6 +2778,8 @@ __metadata:
|
|||
"@backstage/cli": "npm:^0.29.6"
|
||||
"@backstage/config": "npm:^1.3.2"
|
||||
"@backstage/errors": "npm:^1.2.7"
|
||||
"@backstage/plugin-permission-common": "npm:^0.8.4"
|
||||
"@backstage/plugin-permission-node": "npm:^0.8.7"
|
||||
"@backstage/types": "npm:^1.2.1"
|
||||
"@types/express": "npm:^4.17.6"
|
||||
"@types/lodash": "npm:^4.14.151"
|
||||
|
|
|
|||
Loading…
Reference in New Issue