From faf5190d739c54b11d38a5be3c189e22530a9531 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 17 Feb 2024 18:00:42 +0100 Subject: [PATCH] backend-test-utils: refactor mock service auth tokens to mirror creation options Signed-off-by: Patrik Oldsberg --- packages/backend-test-utils/api-report.md | 39 +++++++--- .../src/next/services/MockAuthService.test.ts | 30 ++++++-- .../src/next/services/MockAuthService.ts | 53 ++++++------- .../next/services/MockHttpAuthService.test.ts | 5 +- .../src/next/services/mockCredentials.test.ts | 42 +++-------- .../src/next/services/mockCredentials.ts | 74 +++++++++++++------ 6 files changed, 138 insertions(+), 105 deletions(-) diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index b553d20e48..949caddcfa 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -49,15 +49,22 @@ export function isDockerDisabledForTests(): boolean; // @public (undocumented) export namespace mockCredentials { export function none(): BackstageCredentials; + export namespace none { + export function header(): string; + } export function service( subject?: string, ): BackstageCredentials; export namespace service { - export function header(payload?: TokenPayload): string; - export function token(payload?: TokenPayload): string; - export type TokenPayload = { - subject?: string; - targetPluginId?: string; + export function header(options?: TokenOptions): string; + // (undocumented) + export function invalidHeader(): string; + // (undocumented) + export function invalidToken(): string; + export function token(options?: TokenOptions): string; + export type TokenOptions = { + onBehalfOf: BackstageCredentials; + targetPluginId: string; }; } export function user( @@ -65,10 +72,11 @@ export namespace mockCredentials { ): BackstageCredentials; export namespace user { export function header(userEntityRef?: string): string; + // (undocumented) + export function invalidHeader(): string; + // (undocumented) + export function invalidToken(): string; export function token(userEntityRef?: string): string; - export type TokenPayload = { - userEntityRef?: string; - }; } } @@ -159,12 +167,19 @@ export namespace mockServices { partialImpl?: Partial | undefined, ) => ServiceMock; } - // (undocumented) - export function httpAuth(options?: { pluginId?: string }): HttpAuthService; + export function httpAuth(options?: { + pluginId?: string; + defaultCredentials?: BackstageCredentials; + }): HttpAuthService; // (undocumented) export namespace httpAuth { - const // (undocumented) - factory: () => ServiceFactory; + const factory: ( + options?: + | { + defaultCredentials?: BackstageCredentials | undefined; + } + | undefined, + ) => ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, diff --git a/packages/backend-test-utils/src/next/services/MockAuthService.test.ts b/packages/backend-test-utils/src/next/services/MockAuthService.test.ts index 63a3e83c89..261bc90f77 100644 --- a/packages/backend-test-utils/src/next/services/MockAuthService.test.ts +++ b/packages/backend-test-utils/src/next/services/MockAuthService.test.ts @@ -72,23 +72,32 @@ describe('MockAuthService', () => { auth.authenticate(mockCredentials.service.token()), ).resolves.toEqual(mockCredentials.service(DEFAULT_MOCK_SERVICE_SUBJECT)); + await expect( + auth.authenticate(mockCredentials.service.token()), + ).resolves.toEqual(mockCredentials.service()); + await expect( auth.authenticate( - mockCredentials.service.token({ subject: 'plugin:catalog' }), + mockCredentials.service.token({ + onBehalfOf: mockCredentials.service('plugin:catalog'), + targetPluginId: 'test', + }), ), ).resolves.toEqual(mockCredentials.service('plugin:catalog')); await expect( auth.authenticate( mockCredentials.service.token({ + onBehalfOf: await auth.getOwnServiceCredentials(), targetPluginId: 'test', }), ), - ).resolves.toEqual(mockCredentials.service()); + ).resolves.toEqual(mockCredentials.service('plugin:test')); await expect( auth.authenticate( mockCredentials.service.token({ + onBehalfOf: await auth.getOwnServiceCredentials(), targetPluginId: 'other', }), ), @@ -135,7 +144,12 @@ describe('MockAuthService', () => { onBehalfOf: mockCredentials.user(), targetPluginId: 'test', }), - ).resolves.toEqual({ token: mockCredentials.user.token() }); + ).resolves.toEqual({ + token: mockCredentials.service.token({ + onBehalfOf: mockCredentials.user(), + targetPluginId: 'test', + }), + }); await expect( auth.getPluginRequestToken({ @@ -143,7 +157,10 @@ describe('MockAuthService', () => { targetPluginId: 'test', }), ).resolves.toEqual({ - token: mockCredentials.user.token('user:default/other'), + token: mockCredentials.service.token({ + onBehalfOf: mockCredentials.user('user:default/other'), + targetPluginId: 'test', + }), }); await expect( @@ -153,6 +170,7 @@ describe('MockAuthService', () => { }), ).resolves.toEqual({ token: mockCredentials.service.token({ + onBehalfOf: mockCredentials.service(), targetPluginId: 'test', }), }); @@ -164,7 +182,7 @@ describe('MockAuthService', () => { }), ).resolves.toEqual({ token: mockCredentials.service.token({ - subject: 'external:other', + onBehalfOf: mockCredentials.service('external:other'), targetPluginId: 'test', }), }); @@ -176,7 +194,7 @@ describe('MockAuthService', () => { }), ).resolves.toEqual({ token: mockCredentials.service.token({ - subject: 'plugin:test', + onBehalfOf: await mockCredentials.service('plugin:test'), targetPluginId: 'other', }), }); diff --git a/packages/backend-test-utils/src/next/services/MockAuthService.ts b/packages/backend-test-utils/src/next/services/MockAuthService.ts index b5debd5c7d..5c58240b42 100644 --- a/packages/backend-test-utils/src/next/services/MockAuthService.ts +++ b/packages/backend-test-utils/src/next/services/MockAuthService.ts @@ -29,10 +29,10 @@ import { MOCK_USER_TOKEN_PREFIX, MOCK_SERVICE_TOKEN, MOCK_SERVICE_TOKEN_PREFIX, - DEFAULT_MOCK_USER_ENTITY_REF, - DEFAULT_MOCK_SERVICE_SUBJECT, MOCK_INVALID_USER_TOKEN, MOCK_INVALID_SERVICE_TOKEN, + UserTokenPayload, + ServiceTokenPayload, } from './mockCredentials'; /** @internal */ @@ -55,7 +55,7 @@ export class MockAuthService implements AuthService { } if (token.startsWith(MOCK_USER_TOKEN_PREFIX)) { - const { userEntityRef }: mockCredentials.user.TokenPayload = JSON.parse( + const { sub: userEntityRef }: UserTokenPayload = JSON.parse( token.slice(MOCK_USER_TOKEN_PREFIX.length), ); @@ -63,16 +63,20 @@ export class MockAuthService implements AuthService { } if (token.startsWith(MOCK_SERVICE_TOKEN_PREFIX)) { - const { targetPluginId, subject }: mockCredentials.service.TokenPayload = - JSON.parse(token.slice(MOCK_SERVICE_TOKEN_PREFIX.length)); + const { sub, target, obo }: ServiceTokenPayload = JSON.parse( + token.slice(MOCK_SERVICE_TOKEN_PREFIX.length), + ); - if (targetPluginId && targetPluginId !== this.pluginId) { + if (target && target !== this.pluginId) { throw new AuthenticationError( - `Invalid mock token target plugin ID, got '${targetPluginId}' but expected '${this.pluginId}'`, + `Invalid mock token target plugin ID, got '${target}' but expected '${this.pluginId}'`, ); } + if (obo) { + return mockCredentials.user(obo); + } - return mockCredentials.service(subject); + return mockCredentials.service(sub); } throw new AuthenticationError(`Unknown mock token '${token}'`); @@ -113,28 +117,17 @@ export class MockAuthService implements AuthService { | BackstageServicePrincipal | BackstageNonePrincipal; - switch (principal.type) { - case 'user': - if (principal.userEntityRef === DEFAULT_MOCK_USER_ENTITY_REF) { - return { token: mockCredentials.user.token() }; - } - return { - token: mockCredentials.user.token(principal.userEntityRef), - }; - case 'service': - return { - token: mockCredentials.service.token({ - targetPluginId: options.targetPluginId, - subject: - principal.subject === DEFAULT_MOCK_SERVICE_SUBJECT - ? undefined - : principal.subject, - }), - }; - default: - throw new AuthenticationError( - `Refused to issue service token for credential type '${principal.type}'`, - ); + if (principal.type !== 'user' && principal.type !== 'service') { + throw new AuthenticationError( + `Refused to issue service token for credential type '${principal.type}'`, + ); } + + return { + token: mockCredentials.service.token({ + onBehalfOf: options.onBehalfOf, + targetPluginId: options.targetPluginId, + }), + }; } } diff --git a/packages/backend-test-utils/src/next/services/MockHttpAuthService.test.ts b/packages/backend-test-utils/src/next/services/MockHttpAuthService.test.ts index d441fd0108..6f21c709f7 100644 --- a/packages/backend-test-utils/src/next/services/MockHttpAuthService.test.ts +++ b/packages/backend-test-utils/src/next/services/MockHttpAuthService.test.ts @@ -81,7 +81,10 @@ describe('MockHttpAuthService', () => { await expect( httpAuth.credentials( makeAuthReq( - mockCredentials.service.header({ subject: 'plugin:other' }), + mockCredentials.service.header({ + onBehalfOf: mockCredentials.service('plugin:other'), + targetPluginId: 'test', + }), ), ), ).resolves.toEqual(mockCredentials.service('plugin:other')); diff --git a/packages/backend-test-utils/src/next/services/mockCredentials.test.ts b/packages/backend-test-utils/src/next/services/mockCredentials.test.ts index 7b0ff1e9be..67cda92be8 100644 --- a/packages/backend-test-utils/src/next/services/mockCredentials.test.ts +++ b/packages/backend-test-utils/src/next/services/mockCredentials.test.ts @@ -55,13 +55,13 @@ describe('mockCredentials', () => { it('creates user tokens and headers', () => { expect(mockCredentials.user.token()).toBe('mock-user-token'); expect(mockCredentials.user.token('user:default/other')).toBe( - 'mock-user-token:{"userEntityRef":"user:default/other"}', + 'mock-user-token:{"sub":"user:default/other"}', ); expect(mockCredentials.user.invalidToken()).toBe('mock-invalid-user-token'); expect(mockCredentials.user.header()).toBe('Bearer mock-user-token'); expect(mockCredentials.user.header('user:default/other')).toBe( - 'Bearer mock-user-token:{"userEntityRef":"user:default/other"}', + 'Bearer mock-user-token:{"sub":"user:default/other"}', ); expect(mockCredentials.user.invalidHeader()).toBe( 'Bearer mock-invalid-user-token', @@ -70,60 +70,38 @@ describe('mockCredentials', () => { it('creates service tokens and headers', () => { expect(mockCredentials.service.token()).toBe('mock-service-token'); - expect(mockCredentials.service.token({ subject: 'external:other' })).toBe( - 'mock-service-token:{"subject":"external:other"}', - ); expect( mockCredentials.service.token({ + onBehalfOf: mockCredentials.service('external:other'), targetPluginId: 'other', }), - ).toBe('mock-service-token:{"targetPluginId":"other"}'); + ).toBe('mock-service-token:{"sub":"external:other","target":"other"}'); expect( mockCredentials.service.token({ - subject: 'external:other', + onBehalfOf: mockCredentials.user('user:default/other'), targetPluginId: 'other', }), - ).toBe( - 'mock-service-token:{"subject":"external:other","targetPluginId":"other"}', - ); - // Object keys are reordered, ordering in the token should be stable - expect( - mockCredentials.service.token({ - targetPluginId: 'other', - subject: 'external:other', - }), - ).toBe( - 'mock-service-token:{"subject":"external:other","targetPluginId":"other"}', - ); + ).toBe('mock-service-token:{"obo":"user:default/other","target":"other"}'); expect(mockCredentials.service.invalidToken()).toBe( 'mock-invalid-service-token', ); expect(mockCredentials.service.header()).toBe('Bearer mock-service-token'); - expect(mockCredentials.service.header({ subject: 'external:other' })).toBe( - 'Bearer mock-service-token:{"subject":"external:other"}', - ); expect( mockCredentials.service.header({ - targetPluginId: 'other', - }), - ).toBe('Bearer mock-service-token:{"targetPluginId":"other"}'); - expect( - mockCredentials.service.header({ - subject: 'external:other', + onBehalfOf: mockCredentials.service('external:other'), targetPluginId: 'other', }), ).toBe( - 'Bearer mock-service-token:{"subject":"external:other","targetPluginId":"other"}', + 'Bearer mock-service-token:{"sub":"external:other","target":"other"}', ); - // Object keys are reordered, ordering in the token should be stable expect( mockCredentials.service.header({ + onBehalfOf: mockCredentials.user('user:default/other'), targetPluginId: 'other', - subject: 'external:other', }), ).toBe( - 'Bearer mock-service-token:{"subject":"external:other","targetPluginId":"other"}', + 'Bearer mock-service-token:{"obo":"user:default/other","target":"other"}', ); expect(mockCredentials.service.invalidHeader()).toBe( 'Bearer mock-invalid-service-token', diff --git a/packages/backend-test-utils/src/next/services/mockCredentials.ts b/packages/backend-test-utils/src/next/services/mockCredentials.ts index 3b440b3a31..8dac9c1a4b 100644 --- a/packages/backend-test-utils/src/next/services/mockCredentials.ts +++ b/packages/backend-test-utils/src/next/services/mockCredentials.ts @@ -40,6 +40,24 @@ function validateUserEntityRef(ref: string) { } } +/** + * The payload that can be encoded into a mock user token. + * @internal + */ +export type UserTokenPayload = { + sub?: string; +}; + +/** + * The payload that can be encoded into a mock service token. + * @internal + */ +export type ServiceTokenPayload = { + sub?: string; // service subject + obo?: string; // user entity reference + target?: string; // target plugin id +}; + /** * @public */ @@ -92,11 +110,6 @@ export namespace mockCredentials { * Utilities related to user credentials. */ export namespace user { - /** - * The payload that can be encoded into a mock user token. - */ - export type TokenPayload = { userEntityRef?: string }; - /** * Creates a mocked user token. If a payload is provided it will be encoded * into the token and forwarded to the credentials object when authenticated @@ -105,7 +118,9 @@ export namespace mockCredentials { export function token(userEntityRef?: string): string { if (userEntityRef) { validateUserEntityRef(userEntityRef); - return `${MOCK_USER_TOKEN_PREFIX}${JSON.stringify({ userEntityRef })}`; + return `${MOCK_USER_TOKEN_PREFIX}${JSON.stringify({ + sub: userEntityRef, + } satisfies UserTokenPayload)}`; } return MOCK_USER_TOKEN; } @@ -147,36 +162,47 @@ export namespace mockCredentials { */ export namespace service { /** - * The payload that can be encoded into a mock service token. + * Options for the creation of mock service tokens. */ - export type TokenPayload = { - subject?: string; - targetPluginId?: string; + export type TokenOptions = { + onBehalfOf: BackstageCredentials; + targetPluginId: string; }; /** - * Creates a mocked service token. If a payload is provided it will be - * encoded into the token and forwarded to the credentials object when - * authenticated by the mock auth service. + * Creates a mocked service token. The provided options will be encoded into + * the token and forwarded to the credentials object when authenticated by + * the mock auth service. */ - export function token(payload?: TokenPayload): string { - if (payload) { - const { subject, targetPluginId } = payload; // for fixed ordering + export function token(options?: TokenOptions): string { + if (options) { + const { targetPluginId, onBehalfOf } = options; // for fixed ordering + + const oboPrincipal = onBehalfOf?.principal as + | BackstageServicePrincipal + | BackstageUserPrincipal + | BackstageNonePrincipal; + const obo = + oboPrincipal.type === 'user' ? oboPrincipal.userEntityRef : undefined; + const subject = + oboPrincipal.type === 'service' ? oboPrincipal.subject : undefined; + return `${MOCK_SERVICE_TOKEN_PREFIX}${JSON.stringify({ - subject, - targetPluginId, - })}`; + sub: subject, + obo, + target: targetPluginId, + } satisfies ServiceTokenPayload)}`; } return MOCK_SERVICE_TOKEN; } /** - * Returns an authorization header with a mocked service token. If a - * payload is provided it will be encoded into the token and forwarded to - * the credentials object when authenticated by the mock auth service. + * Returns an authorization header with a mocked service token. The provided + * options will be encoded into the token and forwarded to the credentials + * object when authenticated by the mock auth service. */ - export function header(payload?: TokenPayload): string { - return `Bearer ${token(payload)}`; + export function header(options?: TokenOptions): string { + return `Bearer ${token(options)}`; } export function invalidToken(): string {