refactor(search-backend): use credential for authorized search engines

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2024-02-20 14:14:30 +01:00
parent b7ce9dd933
commit 744c0cbf97
32 changed files with 308 additions and 95 deletions
+7 -1
View File
@@ -3,13 +3,16 @@
> 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 { DocumentTypeInfo } from '@backstage/plugin-search-common';
import express from 'express';
import { HttpAuthService } from '@backstage/backend-plugin-api';
import { Logger } from 'winston';
import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
import { SearchEngine } from '@backstage/plugin-search-common';
import { SearchEngine } from '@backstage/plugin-search-backend-node';
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
@@ -18,8 +21,11 @@ export function createRouter(options: RouterOptions): Promise<express.Router>;
export type RouterOptions = {
engine: SearchEngine;
types: Record<string, DocumentTypeInfo>;
discovery: DiscoveryService;
permissions: PermissionEvaluator | PermissionAuthorizer;
config: Config;
logger: Logger;
auth?: AuthService;
httpAuth?: HttpAuthService;
};
```
+17 -2
View File
@@ -22,6 +22,7 @@ import { loggerToWinstonLogger } from '@backstage/backend-common';
import {
RegisterCollatorParameters,
RegisterDecoratorParameters,
SearchEngine,
LunrSearchEngine,
} from '@backstage/plugin-search-backend-node';
import {
@@ -33,7 +34,6 @@ import {
} from '@backstage/plugin-search-backend-node/alpha';
import { createRouter } from './service/router';
import { SearchEngine } from '@backstage/plugin-search-common';
class SearchIndexRegistry implements SearchIndexRegistryExtensionPoint {
private collators: RegisterCollatorParameters[] = [];
@@ -94,11 +94,23 @@ export default createBackendPlugin({
deps: {
logger: coreServices.logger,
config: coreServices.rootConfig,
discovery: coreServices.discovery,
permissions: coreServices.permissions,
auth: coreServices.auth,
http: coreServices.httpRouter,
httpAuth: coreServices.httpAuth,
searchIndexService: searchIndexServiceRef,
},
async init({ config, logger, permissions, http, searchIndexService }) {
async init({
config,
logger,
discovery,
permissions,
auth,
http,
httpAuth,
searchIndexService,
}) {
let searchEngine = searchEngineRegistry.getSearchEngine();
if (!searchEngine) {
searchEngine = new LunrSearchEngine({
@@ -117,7 +129,10 @@ export default createBackendPlugin({
const router = await createRouter({
config,
discovery,
permissions,
auth,
httpAuth,
logger: loggerToWinstonLogger(logger),
engine: searchEngine,
types: searchIndexService.getDocumentTypes(),
@@ -25,8 +25,8 @@ import {
import {
DocumentTypeInfo,
IndexableDocument,
SearchEngine,
} from '@backstage/plugin-search-common';
import { SearchEngine } from '@backstage/plugin-search-backend-node';
import {
encodePageCursor,
decodePageCursor,
@@ -23,21 +23,23 @@ import {
EvaluatePermissionRequest,
EvaluatePermissionResponse,
isResourcePermission,
PermissionEvaluator,
QueryPermissionRequest,
} from '@backstage/plugin-permission-common';
import {
DocumentTypeInfo,
IndexableResult,
IndexableResultSet,
SearchQuery,
} from '@backstage/plugin-search-common';
import {
QueryRequestOptions,
QueryTranslator,
SearchEngine,
SearchQuery,
} from '@backstage/plugin-search-common';
} from '@backstage/plugin-search-backend-node';
import { Config } from '@backstage/config';
import { InputError } from '@backstage/errors';
import { Writable } from 'stream';
import { PermissionsService } from '@backstage/backend-plugin-api';
export function decodePageCursor(pageCursor?: string): { page: number } {
if (!pageCursor) {
@@ -68,7 +70,7 @@ export class AuthorizedSearchEngine implements SearchEngine {
constructor(
private readonly searchEngine: SearchEngine,
private readonly types: Record<string, DocumentTypeInfo>,
private readonly permissions: PermissionEvaluator,
private readonly permissions: PermissionsService,
config: Config,
) {
this.queryLatencyBudgetMs =
@@ -14,16 +14,22 @@
* limitations under the License.
*/
import { getVoidLogger } from '@backstage/backend-common';
import {
PluginEndpointDiscovery,
getVoidLogger,
} from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
import { IndexBuilder } from '@backstage/plugin-search-backend-node';
import { SearchEngine } from '@backstage/plugin-search-common';
import {
IndexBuilder,
SearchEngine,
} from '@backstage/plugin-search-backend-node';
import express from 'express';
import request from 'supertest';
import { createRouter } from './router';
import { wrapInOpenApiTestServer } from '@backstage/backend-openapi-utils';
import { Server } from 'http';
import { mockCredentials, mockServices } from '@backstage/backend-test-utils';
const mockPermissionEvaluator: PermissionEvaluator = {
authorize: () => {
@@ -38,6 +44,16 @@ describe('createRouter', () => {
let app: express.Express | Server;
let mockSearchEngine: jest.Mocked<SearchEngine>;
const mockBaseUrl = 'http://backstage:9191/api/proxy';
const discovery: PluginEndpointDiscovery = {
async getBaseUrl() {
return mockBaseUrl;
},
async getExternalBaseUrl() {
return mockBaseUrl;
},
};
beforeAll(async () => {
const logger = getVoidLogger();
mockSearchEngine = {
@@ -65,7 +81,10 @@ describe('createRouter', () => {
search: { maxPageLimit: 200, maxTermLength: 20 },
}),
permissions: mockPermissionEvaluator,
discovery,
logger,
auth: mockServices.auth(),
httpAuth: mockServices.httpAuth(),
});
app = wrapInOpenApiTestServer(express().use(router));
});
@@ -227,7 +246,11 @@ describe('createRouter', () => {
unknownKey2: 'unknownValue1',
};
const secondArg = {
token: undefined,
credentials: mockCredentials.user(),
token: mockCredentials.service.token({
onBehalfOf: mockCredentials.user(),
targetPluginId: 'search',
}),
};
expect(response.status).toEqual(200);
expect(mockSearchEngine.query).toHaveBeenCalledWith(firstArg, secondArg);
@@ -251,6 +274,7 @@ describe('createRouter', () => {
types: indexBuilder.getDocumentTypes(),
config: new ConfigReader({ permissions: { enabled: false } }),
permissions: mockPermissionEvaluator,
discovery,
logger,
});
app = express().use(router);
+24 -8
View File
@@ -17,11 +17,13 @@
import express from 'express';
import { Logger } from 'winston';
import { z } from 'zod';
import { errorHandler } from '@backstage/backend-common';
import {
createLegacyAuthAdapters,
errorHandler,
} from '@backstage/backend-common';
import { InputError } from '@backstage/errors';
import { Config } from '@backstage/config';
import { JsonObject, JsonValue } from '@backstage/types';
import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
import {
PermissionAuthorizer,
PermissionEvaluator,
@@ -32,9 +34,14 @@ import {
IndexableResultSet,
SearchResultSet,
} from '@backstage/plugin-search-common';
import { SearchEngine } from '@backstage/plugin-search-common';
import { SearchEngine } from '@backstage/plugin-search-backend-node';
import { AuthorizedSearchEngine } from './AuthorizedSearchEngine';
import { createOpenApiRouter } from '../schema/openapi.generated';
import {
AuthService,
DiscoveryService,
HttpAuthService,
} from '@backstage/backend-plugin-api';
const jsonObjectSchema: z.ZodSchema<JsonObject> = z.lazy(() => {
const jsonValueSchema: z.ZodSchema<JsonValue> = z.lazy(() =>
@@ -57,9 +64,12 @@ const jsonObjectSchema: z.ZodSchema<JsonObject> = z.lazy(() => {
export type RouterOptions = {
engine: SearchEngine;
types: Record<string, DocumentTypeInfo>;
discovery: DiscoveryService;
permissions: PermissionEvaluator | PermissionAuthorizer;
config: Config;
logger: Logger;
auth?: AuthService;
httpAuth?: HttpAuthService;
};
const defaultMaxPageLimit = 100;
@@ -75,6 +85,8 @@ export async function createRouter(
const router = await createOpenApiRouter();
const { engine: inputEngine, types, permissions, config, logger } = options;
const { auth, httpAuth } = createLegacyAuthAdapters(options);
const maxPageLimit =
config.getOptionalNumber('search.maxPageLimit') ?? defaultMaxPageLimit;
@@ -169,12 +181,16 @@ export async function createRouter(
}`,
);
const token = getBearerTokenFromAuthorizationHeader(
req.header('authorization'),
);
try {
const resultSet = await engine?.query(query, { token });
const credentials = await httpAuth.credentials(req);
const { token } = await auth.getPluginRequestToken({
onBehalfOf: credentials,
targetPluginId: 'search',
});
const resultSet = await engine?.query(query, {
token,
credentials,
});
res.json(filterResultSet(toSearchResults(resultSet)));
} catch (error) {
@@ -57,6 +57,7 @@ export async function startStandaloneServer(
const router = await createRouter({
engine: indexBuilder.getSearchEngine(),
types: indexBuilder.getDocumentTypes(),
discovery,
permissions,
config,
logger,