From 25609ed6623226592dbfc72f9fb589b923c90454 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Feb 2024 11:47:37 +0100 Subject: [PATCH] backend-test-utils: add MockHttpAuthService Signed-off-by: Patrik Oldsberg --- .../httpAuth/httpAuthServiceFactory.ts | 2 +- .../src/next/services/MockHttpAuthService.ts | 97 +++++++++++++++++++ .../src/next/services/mockServices.ts | 16 ++- 3 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 packages/backend-test-utils/src/next/services/MockHttpAuthService.ts diff --git a/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts b/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts index 2182e94684..393991c6d0 100644 --- a/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts @@ -63,7 +63,7 @@ type RequestWithCredentials = Request & { [credentialsSymbol]?: Promise; }; -export class DefaultHttpAuthService implements HttpAuthService { +class DefaultHttpAuthService implements HttpAuthService { constructor( private readonly auth: AuthService, private readonly discovery: DiscoveryService, diff --git a/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts b/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts new file mode 100644 index 0000000000..d63d8a1769 --- /dev/null +++ b/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts @@ -0,0 +1,97 @@ +/* + * 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 { + AuthService, + BackstageCredentials, + BackstagePrincipalTypes, + HttpAuthService, +} from '@backstage/backend-plugin-api'; +import { Request, Response } from 'express'; +import { mockCredentials } from './mockCredentials'; +import { MockAuthService } from './MockAuthService'; +import { NotAllowedError, NotImplementedError } from '@backstage/errors'; + +// TODO: support mock cookie auth? +export class MockHttpAuthService implements HttpAuthService { + #auth: AuthService; + + constructor(pluginId: string) { + this.#auth = new MockAuthService(pluginId); + } + + async #getCredentials(req: Request) { + const header = req.headers.authorization; + const token = + typeof header === 'string' + ? header.match(/^Bearer[ ]+(\S+)$/i)?.[1] + : undefined; + if (!token) { + return mockCredentials.none(); + } + + return await this.#auth.authenticate(token); + } + + async credentials( + req: Request, + options?: { + allow?: Array; + allowedAuthMethods?: Array<'token' | 'cookie'>; + }, + ): Promise> { + const credentials = await this.#getCredentials(req); + + const allowedPrincipalTypes = options?.allow; + if (!allowedPrincipalTypes) { + return credentials as any; + } + + if (this.#auth.isPrincipal(credentials, 'unauthenticated')) { + if (allowedPrincipalTypes.includes('none' as TAllowed)) { + return credentials as any; + } + + throw new NotAllowedError( + `This endpoint does not allow 'unauthenticated' credentials`, + ); + } else if (this.#auth.isPrincipal(credentials, 'user')) { + if (allowedPrincipalTypes.includes('user' as TAllowed)) { + return credentials as any; + } + + throw new NotAllowedError( + `This endpoint does not allow 'user' credentials`, + ); + } else if (this.#auth.isPrincipal(credentials, 'service')) { + if (allowedPrincipalTypes.includes('service' as TAllowed)) { + return credentials as any; + } + + throw new NotAllowedError( + `This endpoint does not allow 'service' credentials`, + ); + } + + throw new NotAllowedError( + 'Unknown principal type, this should never happen', + ); + } + + async issueUserCookie(_res: Response): Promise { + throw new NotImplementedError('Not implemented'); + } +} diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 63f8ffbd93..0d88c6b377 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -38,7 +38,6 @@ import { rootLifecycleServiceFactory, schedulerServiceFactory, urlReaderServiceFactory, - httpAuthServiceFactory, discoveryServiceFactory, HostDiscovery, } from '@backstage/backend-app-api'; @@ -47,8 +46,7 @@ import { JsonObject } from '@backstage/types'; import { MockIdentityService } from './MockIdentityService'; import { MockRootLoggerService } from './MockRootLoggerService'; import { MockAuthService } from './MockAuthService'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { DefaultHttpAuthService } from '../../../../backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory'; +import { MockHttpAuthService } from './MockHttpAuthService'; /** @internal */ function simpleFactory< @@ -206,14 +204,14 @@ export namespace mockServices { } export function httpAuth(options?: { pluginId?: string }): HttpAuthService { - return new DefaultHttpAuthService( - auth(), - discovery(), - options?.pluginId ?? 'test', - ); + return new MockHttpAuthService(options?.pluginId ?? 'test'); } export namespace httpAuth { - export const factory = httpAuthServiceFactory; + export const factory = createServiceFactory({ + service: coreServices.httpAuth, + deps: { plugin: coreServices.pluginMetadata }, + factory: ({ plugin }) => new MockHttpAuthService(plugin.getId()), + }); export const mock = simpleMock(coreServices.httpAuth, () => ({ credentials: jest.fn(), issueUserCookie: jest.fn(),