diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index cff8a8fda9..05074c7f60 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -9,6 +9,10 @@ import { AuthService } from '@backstage/backend-plugin-api'; import { Backend } from '@backstage/backend-app-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; +import { BackstageCredentials } from '@backstage/backend-plugin-api'; +import { BackstageNonePrincipal } from '@backstage/backend-plugin-api'; +import { BackstageServicePrincipal } from '@backstage/backend-plugin-api'; +import { BackstageUserPrincipal } from '@backstage/backend-plugin-api'; import { CacheService } from '@backstage/backend-plugin-api'; import { DatabaseService } from '@backstage/backend-plugin-api'; import { DiscoveryService } from '@backstage/backend-plugin-api'; @@ -42,6 +46,30 @@ export function createMockDirectory( // @public (undocumented) export function isDockerDisabledForTests(): boolean; +// @public (undocumented) +export namespace mockCredentials { + export function none(): BackstageCredentials; + export function service( + subject?: string, + ): BackstageCredentials; + export namespace service { + export function token(payload?: TokenPayload): string; + export type TokenPayload = { + subject?: string; + targetPluginId?: string; + }; + } + export function user( + userEntityRef?: string, + ): BackstageCredentials; + export namespace user { + export function token(payload?: TokenPayload): string; + export type TokenPayload = { + userEntityRef: string; + }; + } +} + // @public export interface MockDirectory { addContent(root: MockDirectoryContent): void; diff --git a/packages/backend-test-utils/src/next/services/MockAuthService.ts b/packages/backend-test-utils/src/next/services/MockAuthService.ts index 925ee1bb6d..b73bb7875f 100644 --- a/packages/backend-test-utils/src/next/services/MockAuthService.ts +++ b/packages/backend-test-utils/src/next/services/MockAuthService.ts @@ -23,25 +23,50 @@ import { AuthService, } from '@backstage/backend-plugin-api'; import { AuthenticationError } from '@backstage/errors'; +import { + mockCredentials, + MOCK_USER_TOKEN, + MOCK_USER_TOKEN_PREFIX, + MOCK_SERVICE_TOKEN, + MOCK_SERVICE_TOKEN_PREFIX, + DEFAULT_MOCK_USER_ENTITY_REF, + DEFAULT_MOCK_SERVICE_SUBJECT, +} from './mockCredentials'; /** @internal */ export class MockAuthService implements AuthService { constructor(private readonly pluginId: string) {} async authenticate(token: string): Promise { - if (token === 'mock-user-token') { - return { - $$type: '@backstage/BackstageCredentials', - principal: { type: 'user', userEntityRef: 'user:default/mock' }, - }; - } else if (token === 'mock-service-token') { - return { - $$type: '@backstage/BackstageCredentials', - principal: { type: 'service', subject: 'external:test-service' }, - }; + if (token === MOCK_USER_TOKEN) { + return mockCredentials.user(); + } + if (token === MOCK_SERVICE_TOKEN) { + return mockCredentials.service(); } - throw new AuthenticationError('Invalid token'); + if (token.startsWith(MOCK_USER_TOKEN_PREFIX)) { + const { userEntityRef }: mockCredentials.user.TokenPayload = JSON.parse( + token.slice(MOCK_USER_TOKEN_PREFIX.length), + ); + + return mockCredentials.user(userEntityRef); + } + + if (token.startsWith(MOCK_SERVICE_TOKEN_PREFIX)) { + const { targetPluginId, subject }: mockCredentials.service.TokenPayload = + JSON.parse(token.slice(MOCK_SERVICE_TOKEN_PREFIX.length)); + + if (targetPluginId && targetPluginId !== this.pluginId) { + throw new AuthenticationError( + `Invalid mock token target, got ${targetPluginId} but expected ${this.pluginId}`, + ); + } + + return mockCredentials.service(subject); + } + + throw new AuthenticationError('Invalid mock token'); } async getOwnServiceCredentials(): Promise< @@ -79,9 +104,24 @@ export class MockAuthService implements AuthService { switch (principal.type) { case 'user': - return { token: 'mock-user-token' }; + if (principal.userEntityRef === DEFAULT_MOCK_USER_ENTITY_REF) { + return { token: mockCredentials.user.token() }; + } + return { + token: mockCredentials.user.token({ + userEntityRef: principal.userEntityRef, + }), + }; case 'service': - return { token: `mock-service-token-for-${options.targetPluginId}` }; + 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}'`, diff --git a/packages/backend-test-utils/src/next/services/index.ts b/packages/backend-test-utils/src/next/services/index.ts index 97a22567fd..7c3949e207 100644 --- a/packages/backend-test-utils/src/next/services/index.ts +++ b/packages/backend-test-utils/src/next/services/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export { mockServices, type ServiceMock } from './mockServices'; +export { mockCredentials } from './mockCredentials'; diff --git a/packages/backend-test-utils/src/next/services/mockCredentials.ts b/packages/backend-test-utils/src/next/services/mockCredentials.ts new file mode 100644 index 0000000000..3b77cee9a5 --- /dev/null +++ b/packages/backend-test-utils/src/next/services/mockCredentials.ts @@ -0,0 +1,120 @@ +/* + * 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 { + BackstageCredentials, + BackstageNonePrincipal, + BackstageServicePrincipal, + BackstageUserPrincipal, +} from '@backstage/backend-plugin-api'; + +export const DEFAULT_MOCK_USER_ENTITY_REF = 'user:default/mock'; +export const DEFAULT_MOCK_SERVICE_SUBJECT = 'external:test-service'; + +export const MOCK_USER_TOKEN = 'mock-user-token'; +export const MOCK_USER_TOKEN_PREFIX = 'mock-user-token:'; +export const MOCK_SERVICE_TOKEN = 'mock-service-token'; +export const MOCK_SERVICE_TOKEN_PREFIX = 'mock-service-token:'; + +/** + * @public + */ +export namespace mockCredentials { + /** + * Creates a mocked credentials object for a unauthenticated principal. + */ + export function none(): BackstageCredentials { + return { + $$type: '@backstage/BackstageCredentials', + principal: { type: 'none' }, + }; + } + + /** + * Creates a mocked credentials object for a user principal. + * + * The default user entity reference is 'user:default/mock'. + */ + export function user( + userEntityRef: string = DEFAULT_MOCK_USER_ENTITY_REF, + ): BackstageCredentials { + return { + $$type: '@backstage/BackstageCredentials', + principal: { type: 'user', userEntityRef }, + }; + } + + /** + * 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 + * by the mock auth service. + */ + export function token(payload?: TokenPayload): string { + if (payload) { + return `${MOCK_USER_TOKEN_PREFIX}:${JSON.stringify(payload)}`; + } + return MOCK_USER_TOKEN; + } + } + + /** + * Creates a mocked credentials object for a service principal. + * + * The default subject is 'external:test-service'. + */ + export function service( + subject: string = DEFAULT_MOCK_SERVICE_SUBJECT, + ): BackstageCredentials { + return { + $$type: '@backstage/BackstageCredentials', + principal: { type: 'service', subject }, + }; + } + + /** + * Utilities related to service credentials. + */ + export namespace service { + /** + * The payload that can be encoded into a mock service token. + */ + export type TokenPayload = { + subject?: string; + 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. + */ + export function token(payload?: TokenPayload): string { + if (payload) { + return `${MOCK_SERVICE_TOKEN_PREFIX}:${payload}`; + } + return MOCK_SERVICE_TOKEN; + } + } +}