diff --git a/.changeset/lucky-sheep-cover.md b/.changeset/lucky-sheep-cover.md new file mode 100644 index 0000000000..9b9420a73a --- /dev/null +++ b/.changeset/lucky-sheep-cover.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +The `legacyPlugin` and `makeLegacyPlugin` helpers now provide their own shim implementation of the identity and token manager services, as these services are being removed from the new backend system. diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 1f14c08b50..d0b84edaa6 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -35,6 +35,7 @@ import { PermissionsService } from '@backstage/backend-plugin-api'; import { PluginMetadataService } from '@backstage/backend-plugin-api'; import { PushResult } from 'isomorphic-git'; import { ReadCommitResult } from 'isomorphic-git'; +import { Request as Request_2 } from 'express'; import { RequestHandler } from 'express'; import { resolvePackagePath as resolvePackagePath_2 } from '@backstage/backend-plugin-api'; import { resolveSafeChildPath as resolveSafeChildPath_2 } from '@backstage/backend-plugin-api'; @@ -321,6 +322,23 @@ export type KubernetesContainerRunnerOptions = { // @public @deprecated (undocumented) export type LegacyCreateRouter = (deps: TEnv) => Promise; +// @public @deprecated +export interface LegacyIdentityService { + // (undocumented) + getIdentity(options: { request: Request_2 }): Promise< + | { + expiresInSeconds?: number; + token: string; + identity: { + type: 'user'; + userEntityRef: string; + ownershipEntityRefs: string[]; + }; + } + | undefined + >; +} + // @public @deprecated export const legacyPlugin: ( name: string, @@ -335,9 +353,7 @@ export const legacyPlugin: ( logger: LoggerService; permissions: PermissionsService; scheduler: SchedulerService; - tokenManager: TokenManagerService; reader: UrlReaderService; - identity: IdentityService; }, { logger: (log: LoggerService) => Logger; @@ -345,7 +361,10 @@ export const legacyPlugin: ( getClient(options?: CacheServiceOptions | undefined): CacheService; }; } - > + > & { + tokenManager: TokenManager; + identity: LegacyIdentityService; + } >; }>, ) => BackendFeature; @@ -384,7 +403,12 @@ export function makeLegacyPlugin< ): ( name: string, createRouterImport: Promise<{ - default: LegacyCreateRouter>; + default: LegacyCreateRouter< + TransformedEnv & { + tokenManager: TokenManager; + identity: LegacyIdentityService; + } + >; }>, ) => BackendFeature; diff --git a/packages/backend-common/src/compat/legacy/index.ts b/packages/backend-common/src/compat/legacy/index.ts index 1172084cd7..7e0646342f 100644 --- a/packages/backend-common/src/compat/legacy/index.ts +++ b/packages/backend-common/src/compat/legacy/index.ts @@ -15,4 +15,4 @@ */ export { legacyPlugin, makeLegacyPlugin } from './legacy'; -export type { LegacyCreateRouter } from './legacy'; +export type { LegacyCreateRouter, LegacyIdentityService } from './legacy'; diff --git a/packages/backend-common/src/compat/legacy/legacy.test.ts b/packages/backend-common/src/compat/legacy/legacy.test.ts index 08a645f660..b7a5ff1a78 100644 --- a/packages/backend-common/src/compat/legacy/legacy.test.ts +++ b/packages/backend-common/src/compat/legacy/legacy.test.ts @@ -18,15 +18,18 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { + mockCredentials, + mockServices, + startTestBackend, +} from '@backstage/backend-test-utils'; import { EventEmitter } from 'events'; import { Router } from 'express'; +import request from 'supertest'; import { createLegacyAuthAdapters } from '..'; import { legacyPlugin } from './legacy'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { authServiceFactory } from '../../../../backend-defaults/src/entrypoints/auth'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { tokenManagerServiceFactory } from '../../../../backend-app-api/src/services/implementations/tokenManager'; describe('legacyPlugin', () => { it('can auth across the new and old systems', async () => { @@ -41,7 +44,6 @@ describe('legacyPlugin', () => { await startTestBackend({ features: [ authServiceFactory, - tokenManagerServiceFactory, mockServices.rootConfig.factory({ data: { backend: { @@ -115,4 +117,54 @@ describe('legacyPlugin', () => { await done; }); + + it('can auth users with the identity service shim', async () => { + const backend = await startTestBackend({ + features: [ + mockServices.rootConfig.factory({ + data: { + backend: { + auth: { + keys: [ + { + secret: 'test', + }, + ], + }, + }, + }, + }), + legacyPlugin( + 'test', + Promise.resolve({ + async default({ identity }) { + const router = Router(); + + router.get('/', async (req, res) => { + const user = await identity.getIdentity({ request: req }); + res.json(user); + }); + + return router; + }, + }), + ), + ], + }); + + const res = await request(backend.server) + .get('/api/test') + .set('authorization', mockCredentials.user.header()); + + const mockUserRef = mockCredentials.user().principal.userEntityRef; + expect(res.status).toBe(200); + expect(res.body).toEqual({ + token: mockCredentials.user.token(), + identity: { + type: 'user', + userEntityRef: mockUserRef, + ownershipEntityRefs: [mockUserRef], + }, + }); + }); }); diff --git a/packages/backend-common/src/compat/legacy/legacy.ts b/packages/backend-common/src/compat/legacy/legacy.ts index 4fc89baee2..bf051f3040 100644 --- a/packages/backend-common/src/compat/legacy/legacy.ts +++ b/packages/backend-common/src/compat/legacy/legacy.ts @@ -18,13 +18,16 @@ import { AuthService, coreServices, createBackendPlugin, - HttpRouterService, + LoggerService, + RootConfigService, ServiceRef, + UserInfoService, } from '@backstage/backend-plugin-api'; import { RequestHandler } from 'express'; import { cacheToPluginCacheManager } from '../cache'; import { loggerToWinstonLogger } from '../logging'; -import { TokenManager } from '../../deprecated'; +import { ServerTokenManager, TokenManager } from '../../deprecated'; +import { Request } from 'express'; /** * @public @@ -46,8 +49,13 @@ type TransformedEnv< // new plugin tokens, which we'll also be signaling by supporting the JWKS endpoint through // the http router. // This makes sure that we accept the new plugin tokens as valid tokens, but otherwise fall -// back to whatever the token manager is doing. -function wrapTokenManager(tokenManager: TokenManager, auth: AuthService) { +// back to the legacy token manager. +function createTokenManagerShim( + auth: AuthService, + config: RootConfigService, + logger: LoggerService, +): TokenManager { + const tokenManager = ServerTokenManager.fromConfig(config, { logger }); return { async getToken() { return tokenManager.getToken(); @@ -64,7 +72,64 @@ function wrapTokenManager(tokenManager: TokenManager, auth: AuthService) { } await tokenManager.authenticate(token); }, - } satisfies TokenManager; + }; +} + +/** + * Originally IdentityApi from `@backstage/plugin-auth-node`, re-declared here for backwards compatibility + * @public + * @deprecated Only relevant for legacy plugins, which are deprecated. + */ +export interface LegacyIdentityService { + getIdentity(options: { request: Request }): Promise< + | { + expiresInSeconds?: number; + + token: string; + identity: { + type: 'user'; + userEntityRef: string; + ownershipEntityRefs: string[]; + }; + } + | undefined + >; +} + +// This doesn't use DefaultIdentityClient because we will be removing it and break support for ownershipEntityRefs +function createIdentityServiceShim( + auth: AuthService, + userInfo: UserInfoService, +): LegacyIdentityService { + return { + async getIdentity(options) { + const authHeader = options.request.headers.authorization; + if (typeof authHeader !== 'string') { + return undefined; + } + + const token = authHeader.match(/^Bearer[ ]+(\S+)$/i)?.[1]; + if (!token) { + return undefined; + } + + const credentials = await auth.authenticate(token); + if (!auth.isPrincipal(credentials, 'user')) { + return undefined; + } + + const info = await userInfo.getUserInfo(credentials); + + return { + token, + identity: { + type: 'user', + userEntityRef: info.userEntityRef, + ownershipEntityRefs: info.ownershipEntityRefs, + }, + }; + }, + }; } /** @@ -87,7 +152,12 @@ export function makeLegacyPlugin< return ( name: string, createRouterImport: Promise<{ - default: LegacyCreateRouter>; + default: LegacyCreateRouter< + TransformedEnv & { + tokenManager: TokenManager; + identity: LegacyIdentityService; + } + >; }>, ) => { return createBackendPlugin({ @@ -96,10 +166,20 @@ export function makeLegacyPlugin< env.registerInit({ deps: { ...envMapping, - _router: coreServices.httpRouter, - _auth: coreServices.auth, + $$router: coreServices.httpRouter, + $$auth: coreServices.auth, + $$userInfo: coreServices.userInfo, + $$config: coreServices.rootConfig, + $$logger: coreServices.logger, }, - async init({ _router, _auth, ...envDeps }) { + async init({ + $$auth, + $$config, + $$logger, + $$router, + $$userInfo, + ...envDeps + }) { const { default: createRouter } = await createRouterImport; const pluginEnv = Object.fromEntries( Object.entries(envDeps).map(([key, dep]) => { @@ -107,19 +187,31 @@ export function makeLegacyPlugin< if (transform) { return [key, transform(dep)]; } - if (key === 'tokenManager') { - return [ - key, - wrapTokenManager(dep as TokenManager, _auth as AuthService), - ]; - } return [key, dep]; }), ); - const router = await createRouter( - pluginEnv as TransformedEnv, + + const auth = $$auth as typeof coreServices.auth.T; + const config = $$config as typeof coreServices.rootConfig.T; + const logger = $$logger as typeof coreServices.logger.T; + const router = $$router as typeof coreServices.httpRouter.T; + const userInfo = $$userInfo as typeof coreServices.userInfo.T; + + // Token manager and identity services are no longer supported in the new backend system, so we provide shims for them. + pluginEnv.tokenManager = createTokenManagerShim( + auth, + config, + logger, ); - (_router as HttpRouterService).use(router); + pluginEnv.identity = createIdentityServiceShim(auth, userInfo); + + const pluginRouter = await createRouter( + pluginEnv as TransformedEnv & { + tokenManager: TokenManager; + identity: LegacyIdentityService; + }, + ); + router.use(pluginRouter); }, }); }, @@ -153,9 +245,7 @@ export const legacyPlugin = makeLegacyPlugin( logger: coreServices.logger, permissions: coreServices.permissions, scheduler: coreServices.scheduler, - tokenManager: coreServices.tokenManager, reader: coreServices.urlReader, - identity: coreServices.identity, }, { logger: log => loggerToWinstonLogger(log),