Merge pull request #23217 from backstage/freben/permission-node
auth: convert permission-backend to the new auth services
This commit is contained in:
@@ -3,27 +3,36 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AuthService } from '@backstage/backend-plugin-api';
|
||||
import { Config } from '@backstage/config';
|
||||
import { DiscoveryService } from '@backstage/backend-plugin-api';
|
||||
import express from 'express';
|
||||
import { HttpAuthService } from '@backstage/backend-plugin-api';
|
||||
import { IdentityApi } from '@backstage/plugin-auth-node';
|
||||
import { Logger } from 'winston';
|
||||
import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { UserInfoService } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @public
|
||||
export function createRouter(options: RouterOptions): Promise<express.Router>;
|
||||
|
||||
// @public
|
||||
export interface RouterOptions {
|
||||
// (undocumented)
|
||||
auth?: AuthService;
|
||||
// (undocumented)
|
||||
config: Config;
|
||||
// (undocumented)
|
||||
discovery: PluginEndpointDiscovery;
|
||||
discovery: DiscoveryService;
|
||||
// (undocumented)
|
||||
identity: IdentityApi;
|
||||
httpAuth?: HttpAuthService;
|
||||
// (undocumented)
|
||||
identity?: IdentityApi;
|
||||
// (undocumented)
|
||||
logger: Logger;
|
||||
// (undocumented)
|
||||
policy: PermissionPolicy;
|
||||
// (undocumented)
|
||||
userInfo?: UserInfoService;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/lodash": "^4.14.151",
|
||||
"@types/supertest": "^2.0.8",
|
||||
|
||||
@@ -55,9 +55,19 @@ export const permissionPlugin = createBackendPlugin({
|
||||
config: coreServices.rootConfig,
|
||||
logger: coreServices.logger,
|
||||
discovery: coreServices.discovery,
|
||||
identity: coreServices.identity,
|
||||
auth: coreServices.auth,
|
||||
httpAuth: coreServices.httpAuth,
|
||||
userInfo: coreServices.userInfo,
|
||||
},
|
||||
async init({ http, config, logger, discovery, identity }) {
|
||||
async init({
|
||||
http,
|
||||
config,
|
||||
logger,
|
||||
discovery,
|
||||
auth,
|
||||
httpAuth,
|
||||
userInfo,
|
||||
}) {
|
||||
const winstonLogger = loggerToWinstonLogger(logger);
|
||||
if (!policies.policy) {
|
||||
throw new Error(
|
||||
@@ -69,9 +79,11 @@ export const permissionPlugin = createBackendPlugin({
|
||||
await createRouter({
|
||||
config,
|
||||
discovery,
|
||||
identity,
|
||||
logger: winstonLogger,
|
||||
policy: policies.policy,
|
||||
auth,
|
||||
httpAuth,
|
||||
userInfo,
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ import { Server } from 'http';
|
||||
import express, { Router, RequestHandler } from 'express';
|
||||
import { RestContext, rest } from 'msw';
|
||||
import { setupServer, SetupServer } from 'msw/node';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { mockCredentials, mockServices } from '@backstage/backend-test-utils';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PermissionCondition,
|
||||
@@ -31,10 +31,12 @@ import {
|
||||
} from '@backstage/plugin-permission-node';
|
||||
import { PermissionIntegrationClient } from './PermissionIntegrationClient';
|
||||
import { z } from 'zod';
|
||||
import { DiscoveryService } from '@backstage/backend-plugin-api';
|
||||
|
||||
describe('PermissionIntegrationClient', () => {
|
||||
describe('applyConditions', () => {
|
||||
let server: SetupServer;
|
||||
const auth = mockServices.auth();
|
||||
|
||||
const mockConditions: PermissionCriteria<PermissionCondition> = {
|
||||
not: {
|
||||
@@ -58,7 +60,7 @@ describe('PermissionIntegrationClient', () => {
|
||||
);
|
||||
|
||||
const mockBaseUrl = 'http://backstage:9191';
|
||||
const discovery: PluginEndpointDiscovery = {
|
||||
const discovery: DiscoveryService = {
|
||||
async getBaseUrl(pluginId) {
|
||||
return `${mockBaseUrl}/${pluginId}`;
|
||||
},
|
||||
@@ -70,6 +72,7 @@ describe('PermissionIntegrationClient', () => {
|
||||
const client: PermissionIntegrationClient = new PermissionIntegrationClient(
|
||||
{
|
||||
discovery,
|
||||
auth,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -91,7 +94,7 @@ describe('PermissionIntegrationClient', () => {
|
||||
});
|
||||
|
||||
it('should make a POST request to the correct endpoint', async () => {
|
||||
await client.applyConditions('plugin-1', [
|
||||
await client.applyConditions('plugin-1', mockCredentials.none(), [
|
||||
{
|
||||
id: '123',
|
||||
resourceRef: 'testResource1',
|
||||
@@ -104,7 +107,7 @@ describe('PermissionIntegrationClient', () => {
|
||||
});
|
||||
|
||||
it('should include a request body', async () => {
|
||||
await client.applyConditions('plugin-1', [
|
||||
await client.applyConditions('plugin-1', mockCredentials.none(), [
|
||||
{
|
||||
id: '123',
|
||||
resourceRef: 'testResource1',
|
||||
@@ -132,14 +135,18 @@ describe('PermissionIntegrationClient', () => {
|
||||
});
|
||||
|
||||
it('should return the response from the fetch request', async () => {
|
||||
const response = await client.applyConditions('plugin-1', [
|
||||
{
|
||||
id: '123',
|
||||
resourceRef: 'testResource1',
|
||||
resourceType: 'test-resource',
|
||||
conditions: mockConditions,
|
||||
},
|
||||
]);
|
||||
const response = await client.applyConditions(
|
||||
'plugin-1',
|
||||
mockCredentials.none(),
|
||||
[
|
||||
{
|
||||
id: '123',
|
||||
resourceRef: 'testResource1',
|
||||
resourceType: 'test-resource',
|
||||
conditions: mockConditions,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
expect(response).toEqual(
|
||||
expect.objectContaining([{ id: '123', result: AuthorizeResult.ALLOW }]),
|
||||
@@ -147,7 +154,7 @@ describe('PermissionIntegrationClient', () => {
|
||||
});
|
||||
|
||||
it('should not include authorization headers if no token is supplied', async () => {
|
||||
await client.applyConditions('plugin-1', [
|
||||
await client.applyConditions('plugin-1', mockCredentials.none(), [
|
||||
{
|
||||
id: '123',
|
||||
resourceRef: 'testResource1',
|
||||
@@ -161,21 +168,22 @@ describe('PermissionIntegrationClient', () => {
|
||||
});
|
||||
|
||||
it('should include correctly-constructed authorization header if token is supplied', async () => {
|
||||
await client.applyConditions(
|
||||
'plugin-1',
|
||||
[
|
||||
{
|
||||
id: '123',
|
||||
resourceRef: 'testResource1',
|
||||
resourceType: 'test-resource',
|
||||
conditions: mockConditions,
|
||||
},
|
||||
],
|
||||
'Bearer fake-token',
|
||||
);
|
||||
await client.applyConditions('plugin-1', mockCredentials.user(), [
|
||||
{
|
||||
id: '123',
|
||||
resourceRef: 'testResource1',
|
||||
resourceType: 'test-resource',
|
||||
conditions: mockConditions,
|
||||
},
|
||||
]);
|
||||
|
||||
const request = mockApplyConditionsHandler.mock.calls[0][0];
|
||||
expect(request.headers.get('authorization')).toEqual('Bearer fake-token');
|
||||
expect(request.headers.get('authorization')).toEqual(
|
||||
mockCredentials.service.header({
|
||||
onBehalfOf: mockCredentials.user(),
|
||||
targetPluginId: 'plugin-1',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should forward response errors', async () => {
|
||||
@@ -186,7 +194,7 @@ describe('PermissionIntegrationClient', () => {
|
||||
);
|
||||
|
||||
await expect(
|
||||
client.applyConditions('plugin-1', [
|
||||
client.applyConditions('plugin-1', mockCredentials.none(), [
|
||||
{
|
||||
id: '123',
|
||||
resourceRef: 'testResource1',
|
||||
@@ -194,7 +202,7 @@ describe('PermissionIntegrationClient', () => {
|
||||
conditions: mockConditions,
|
||||
},
|
||||
]),
|
||||
).rejects.toThrow(/401/i);
|
||||
).rejects.toThrow(/401/);
|
||||
});
|
||||
|
||||
it('should reject invalid responses', async () => {
|
||||
@@ -207,7 +215,7 @@ describe('PermissionIntegrationClient', () => {
|
||||
);
|
||||
|
||||
await expect(
|
||||
client.applyConditions('plugin-1', [
|
||||
client.applyConditions('plugin-1', mockCredentials.none(), [
|
||||
{
|
||||
id: '123',
|
||||
resourceRef: 'testResource1',
|
||||
@@ -234,7 +242,7 @@ describe('PermissionIntegrationClient', () => {
|
||||
);
|
||||
|
||||
await expect(
|
||||
client.applyConditions('plugin-1', [
|
||||
client.applyConditions('plugin-1', mockCredentials.none(), [
|
||||
{
|
||||
id: '123',
|
||||
resourceRef: 'testResource1',
|
||||
@@ -268,6 +276,7 @@ describe('PermissionIntegrationClient', () => {
|
||||
let server: Server;
|
||||
let client: PermissionIntegrationClient;
|
||||
let routerSpy: RequestHandler;
|
||||
const auth = mockServices.auth();
|
||||
|
||||
beforeAll(async () => {
|
||||
const router = Router();
|
||||
@@ -319,7 +328,7 @@ describe('PermissionIntegrationClient', () => {
|
||||
server = app.listen(resolve);
|
||||
});
|
||||
|
||||
const discovery: PluginEndpointDiscovery = {
|
||||
const discovery: DiscoveryService = {
|
||||
async getBaseUrl(pluginId: string) {
|
||||
const listenPort = (server.address()! as AddressInfo).port;
|
||||
|
||||
@@ -332,6 +341,7 @@ describe('PermissionIntegrationClient', () => {
|
||||
|
||||
client = new PermissionIntegrationClient({
|
||||
discovery,
|
||||
auth,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -348,7 +358,7 @@ describe('PermissionIntegrationClient', () => {
|
||||
|
||||
it('works for simple conditions', async () => {
|
||||
await expect(
|
||||
client.applyConditions('plugin-1', [
|
||||
client.applyConditions('plugin-1', mockCredentials.none(), [
|
||||
{
|
||||
id: '123',
|
||||
resourceRef: 'testResource1',
|
||||
@@ -367,7 +377,7 @@ describe('PermissionIntegrationClient', () => {
|
||||
|
||||
it('works for complex criteria', async () => {
|
||||
await expect(
|
||||
client.applyConditions('plugin-1', [
|
||||
client.applyConditions('plugin-1', mockCredentials.none(), [
|
||||
{
|
||||
id: '123',
|
||||
resourceRef: 'testResource1',
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
import fetch from 'node-fetch';
|
||||
import { z } from 'zod';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
ConditionalPolicyDecision,
|
||||
@@ -25,6 +24,11 @@ import {
|
||||
ApplyConditionsRequestEntry,
|
||||
ApplyConditionsResponseEntry,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
import {
|
||||
AuthService,
|
||||
BackstageCredentials,
|
||||
DiscoveryService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
const responseSchema = z.object({
|
||||
items: z.array(
|
||||
@@ -42,20 +46,30 @@ export type ResourcePolicyDecision = ConditionalPolicyDecision & {
|
||||
};
|
||||
|
||||
export class PermissionIntegrationClient {
|
||||
private readonly discovery: PluginEndpointDiscovery;
|
||||
private readonly discovery: DiscoveryService;
|
||||
private readonly auth: AuthService;
|
||||
|
||||
constructor(options: { discovery: PluginEndpointDiscovery }) {
|
||||
constructor(options: { discovery: DiscoveryService; auth: AuthService }) {
|
||||
this.discovery = options.discovery;
|
||||
this.auth = options.auth;
|
||||
}
|
||||
|
||||
async applyConditions(
|
||||
pluginId: string,
|
||||
credentials: BackstageCredentials,
|
||||
decisions: readonly ApplyConditionsRequestEntry[],
|
||||
authHeader?: string,
|
||||
): Promise<ApplyConditionsResponseEntry[]> {
|
||||
const endpoint = `${await this.discovery.getBaseUrl(
|
||||
pluginId,
|
||||
)}/.well-known/backstage/permissions/apply-conditions`;
|
||||
const baseUrl = await this.discovery.getBaseUrl(pluginId);
|
||||
const endpoint = `${baseUrl}/.well-known/backstage/permissions/apply-conditions`;
|
||||
|
||||
const token = this.auth.isPrincipal(credentials, 'none')
|
||||
? undefined
|
||||
: await this.auth
|
||||
.getPluginRequestToken({
|
||||
onBehalfOf: credentials,
|
||||
targetPluginId: pluginId,
|
||||
})
|
||||
.then(t => t.token);
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
@@ -70,7 +84,7 @@ export class PermissionIntegrationClient {
|
||||
),
|
||||
}),
|
||||
headers: {
|
||||
...(authHeader ? { authorization: authHeader } : {}),
|
||||
...(token ? { authorization: `Bearer ${token}` } : {}),
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -26,12 +26,15 @@ import { PermissionIntegrationClient } from './PermissionIntegrationClient';
|
||||
|
||||
import { createRouter } from './router';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { BackstageCredentials } from '@backstage/backend-plugin-api';
|
||||
import { mockCredentials, mockServices } from '@backstage/backend-test-utils';
|
||||
|
||||
const mockApplyConditions: jest.MockedFunction<
|
||||
InstanceType<typeof PermissionIntegrationClient>['applyConditions']
|
||||
> = jest.fn(
|
||||
async (
|
||||
_pluginId: string,
|
||||
_credentials: BackstageCredentials,
|
||||
decisions: readonly ApplyConditionsRequestEntry[],
|
||||
) =>
|
||||
decisions.map(decision => ({
|
||||
@@ -65,28 +68,12 @@ describe('createRouter', () => {
|
||||
const router = await createRouter({
|
||||
config: new ConfigReader({ permission: { enabled: true } }),
|
||||
logger: getVoidLogger(),
|
||||
discovery: {
|
||||
getBaseUrl: jest.fn(),
|
||||
getExternalBaseUrl: jest.fn(),
|
||||
},
|
||||
identity: {
|
||||
getIdentity: jest.fn(({ request: req }) => {
|
||||
const token = req.headers.authorization?.replace(/^Bearer[ ]+/, '');
|
||||
|
||||
if (!token) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
identity: {
|
||||
type: 'user',
|
||||
userEntityRef: 'test-user',
|
||||
ownershipEntityRefs: ['blah'],
|
||||
},
|
||||
token,
|
||||
});
|
||||
}),
|
||||
},
|
||||
discovery: mockServices.discovery(),
|
||||
auth: mockServices.auth(),
|
||||
httpAuth: mockServices.httpAuth({
|
||||
defaultCredentials: mockCredentials.none(),
|
||||
}),
|
||||
userInfo: mockServices.userInfo(),
|
||||
policy,
|
||||
});
|
||||
|
||||
@@ -163,10 +150,9 @@ describe('createRouter', () => {
|
||||
});
|
||||
|
||||
it('resolves identity from the Authorization header', async () => {
|
||||
const token = 'test-token';
|
||||
const response = await request(app)
|
||||
.post('/authorize')
|
||||
.auth(token, { type: 'bearer' })
|
||||
.auth(mockCredentials.user.token(), { type: 'bearer' })
|
||||
.send({
|
||||
items: [
|
||||
{
|
||||
@@ -190,11 +176,16 @@ describe('createRouter', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
token: 'test-token',
|
||||
token: mockCredentials.service.token({
|
||||
onBehalfOf: mockCredentials.user(),
|
||||
targetPluginId: 'catalog',
|
||||
}),
|
||||
identity: {
|
||||
type: 'user',
|
||||
userEntityRef: 'test-user',
|
||||
ownershipEntityRefs: ['blah'],
|
||||
userEntityRef: mockCredentials.user().principal.userEntityRef,
|
||||
ownershipEntityRefs: [
|
||||
mockCredentials.user().principal.userEntityRef,
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -271,7 +262,7 @@ describe('createRouter', () => {
|
||||
|
||||
const response = await request(app)
|
||||
.post('/authorize')
|
||||
.auth('test-token', { type: 'bearer' })
|
||||
.auth(mockCredentials.user.token(), { type: 'bearer' })
|
||||
.send({
|
||||
items: [
|
||||
{
|
||||
@@ -319,6 +310,7 @@ describe('createRouter', () => {
|
||||
|
||||
expect(mockApplyConditions).toHaveBeenCalledWith(
|
||||
'plugin-1',
|
||||
mockCredentials.user(),
|
||||
[
|
||||
expect.objectContaining({
|
||||
id: '123',
|
||||
@@ -333,11 +325,11 @@ describe('createRouter', () => {
|
||||
conditions: { rule: 'test-rule', params: ['no'] },
|
||||
}),
|
||||
],
|
||||
'Bearer test-token',
|
||||
);
|
||||
|
||||
expect(mockApplyConditions).toHaveBeenCalledWith(
|
||||
'plugin-2',
|
||||
mockCredentials.user(),
|
||||
[
|
||||
expect.objectContaining({
|
||||
id: '234',
|
||||
@@ -352,7 +344,6 @@ describe('createRouter', () => {
|
||||
conditions: { rule: 'test-rule', params: ['no'] },
|
||||
}),
|
||||
],
|
||||
'Bearer test-token',
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
@@ -401,7 +392,7 @@ describe('createRouter', () => {
|
||||
|
||||
const response = await request(app)
|
||||
.post('/authorize')
|
||||
.auth('test-token', { type: 'bearer' })
|
||||
.auth(mockCredentials.user.token(), { type: 'bearer' })
|
||||
.send({
|
||||
items: [
|
||||
{
|
||||
@@ -467,6 +458,7 @@ describe('createRouter', () => {
|
||||
|
||||
expect(mockApplyConditions).toHaveBeenCalledWith(
|
||||
'plugin-1',
|
||||
mockCredentials.user(),
|
||||
[
|
||||
expect.objectContaining({
|
||||
id: '123',
|
||||
@@ -481,11 +473,11 @@ describe('createRouter', () => {
|
||||
conditions: { rule: 'test-rule', params: ['yes'] },
|
||||
}),
|
||||
],
|
||||
'Bearer test-token',
|
||||
);
|
||||
|
||||
expect(mockApplyConditions).toHaveBeenCalledWith(
|
||||
'plugin-2',
|
||||
mockCredentials.user(),
|
||||
[
|
||||
expect.objectContaining({
|
||||
id: '234',
|
||||
@@ -500,7 +492,6 @@ describe('createRouter', () => {
|
||||
conditions: { rule: 'test-rule', params: ['yes'] },
|
||||
}),
|
||||
],
|
||||
'Bearer test-token',
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
@@ -542,7 +533,7 @@ describe('createRouter', () => {
|
||||
|
||||
const response = await request(app)
|
||||
.post('/authorize')
|
||||
.auth('test-token', { type: 'bearer' })
|
||||
.auth(mockCredentials.user.token(), { type: 'bearer' })
|
||||
.send({
|
||||
items: [
|
||||
{
|
||||
@@ -589,6 +580,7 @@ describe('createRouter', () => {
|
||||
|
||||
expect(mockApplyConditions).toHaveBeenCalledWith(
|
||||
'plugin-1',
|
||||
mockCredentials.user(),
|
||||
[
|
||||
expect.objectContaining({
|
||||
id: '123',
|
||||
@@ -597,11 +589,11 @@ describe('createRouter', () => {
|
||||
conditions: { rule: 'test-rule', params: ['yes'] },
|
||||
}),
|
||||
],
|
||||
'Bearer test-token',
|
||||
);
|
||||
|
||||
expect(mockApplyConditions).toHaveBeenCalledWith(
|
||||
'plugin-2',
|
||||
mockCredentials.user(),
|
||||
[
|
||||
expect.objectContaining({
|
||||
id: '234',
|
||||
@@ -610,7 +602,6 @@ describe('createRouter', () => {
|
||||
conditions: { rule: 'test-rule', params: ['yes'] },
|
||||
}),
|
||||
],
|
||||
'Bearer test-token',
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
@@ -656,7 +647,7 @@ describe('createRouter', () => {
|
||||
|
||||
const response = await request(app)
|
||||
.post('/authorize')
|
||||
.auth('test-token', { type: 'bearer' })
|
||||
.auth(mockCredentials.user.token(), { type: 'bearer' })
|
||||
.send({
|
||||
items: [
|
||||
{
|
||||
@@ -684,6 +675,7 @@ describe('createRouter', () => {
|
||||
|
||||
expect(mockApplyConditions).toHaveBeenCalledWith(
|
||||
'test-plugin',
|
||||
mockCredentials.user(),
|
||||
[
|
||||
expect.objectContaining({
|
||||
id: '123',
|
||||
@@ -698,7 +690,6 @@ describe('createRouter', () => {
|
||||
conditions: { rule: 'test-rule', params },
|
||||
}),
|
||||
],
|
||||
'Bearer test-token',
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
|
||||
@@ -19,8 +19,8 @@ import express, { Request, Response } from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import {
|
||||
createLegacyAuthAdapters,
|
||||
errorHandler,
|
||||
PluginEndpointDiscovery,
|
||||
} from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import {
|
||||
@@ -46,6 +46,15 @@ import { PermissionIntegrationClient } from './PermissionIntegrationClient';
|
||||
import { memoize } from 'lodash';
|
||||
import DataLoader from 'dataloader';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
AuthService,
|
||||
BackstageCredentials,
|
||||
BackstageNonePrincipal,
|
||||
BackstageUserPrincipal,
|
||||
DiscoveryService,
|
||||
HttpAuthService,
|
||||
UserInfoService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
const attributesSchema: z.ZodSchema<PermissionAttributes> = z.object({
|
||||
action: z
|
||||
@@ -93,28 +102,51 @@ const evaluatePermissionRequestBatchSchema: z.ZodSchema<EvaluatePermissionReques
|
||||
*/
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
discovery: PluginEndpointDiscovery;
|
||||
discovery: DiscoveryService;
|
||||
policy: PermissionPolicy;
|
||||
identity: IdentityApi;
|
||||
identity?: IdentityApi;
|
||||
config: Config;
|
||||
auth?: AuthService;
|
||||
httpAuth?: HttpAuthService;
|
||||
userInfo?: UserInfoService;
|
||||
}
|
||||
|
||||
const handleRequest = async (
|
||||
requests: IdentifiedPermissionMessage<EvaluatePermissionRequest>[],
|
||||
user: BackstageIdentityResponse | undefined,
|
||||
policy: PermissionPolicy,
|
||||
permissionIntegrationClient: PermissionIntegrationClient,
|
||||
authHeader?: string,
|
||||
credentials: BackstageCredentials<
|
||||
BackstageNonePrincipal | BackstageUserPrincipal
|
||||
>,
|
||||
auth: AuthService,
|
||||
userInfo: UserInfoService,
|
||||
): Promise<IdentifiedPermissionMessage<EvaluatePermissionResponse>[]> => {
|
||||
const applyConditionsLoaderFor = memoize((pluginId: string) => {
|
||||
return new DataLoader<
|
||||
ApplyConditionsRequestEntry,
|
||||
ApplyConditionsResponseEntry
|
||||
>(batch =>
|
||||
permissionIntegrationClient.applyConditions(pluginId, batch, authHeader),
|
||||
permissionIntegrationClient.applyConditions(pluginId, credentials, batch),
|
||||
);
|
||||
});
|
||||
|
||||
let user: BackstageIdentityResponse | undefined;
|
||||
if (auth.isPrincipal(credentials, 'user')) {
|
||||
const { ownershipEntityRefs } = await userInfo.getUserInfo(credentials);
|
||||
const { token } = await auth.getPluginRequestToken({
|
||||
onBehalfOf: credentials,
|
||||
targetPluginId: 'catalog', // TODO: unknown at this point
|
||||
});
|
||||
user = {
|
||||
identity: {
|
||||
type: 'user',
|
||||
userEntityRef: credentials.principal.userEntityRef,
|
||||
ownershipEntityRefs,
|
||||
},
|
||||
token,
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
requests.map(({ id, resourceRef, ...request }) =>
|
||||
policy.handle(request, user).then(decision => {
|
||||
@@ -163,7 +195,8 @@ const handleRequest = async (
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const { policy, discovery, identity, config, logger } = options;
|
||||
const { policy, discovery, config, logger } = options;
|
||||
const { auth, httpAuth, userInfo } = createLegacyAuthAdapters(options);
|
||||
|
||||
if (!config.getOptionalBoolean('permission.enabled')) {
|
||||
logger.warn(
|
||||
@@ -173,6 +206,7 @@ export async function createRouter(
|
||||
|
||||
const permissionIntegrationClient = new PermissionIntegrationClient({
|
||||
discovery,
|
||||
auth,
|
||||
});
|
||||
|
||||
const router = Router();
|
||||
@@ -188,7 +222,9 @@ export async function createRouter(
|
||||
req: Request<EvaluatePermissionRequestBatch>,
|
||||
res: Response<EvaluatePermissionResponseBatch>,
|
||||
) => {
|
||||
const user = await identity.getIdentity({ request: req });
|
||||
const credentials = await httpAuth.credentials(req, {
|
||||
allow: ['user', 'none'],
|
||||
});
|
||||
|
||||
const parseResult = evaluatePermissionRequestBatchSchema.safeParse(
|
||||
req.body,
|
||||
@@ -203,10 +239,11 @@ export async function createRouter(
|
||||
res.json({
|
||||
items: await handleRequest(
|
||||
body.items,
|
||||
user,
|
||||
policy,
|
||||
permissionIntegrationClient,
|
||||
req.header('authorization'),
|
||||
credentials,
|
||||
auth,
|
||||
userInfo,
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user