From fa8d1a7c6afa6c350eecbae5140afa9cac1c9d3b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 21 Aug 2024 01:32:09 +0200 Subject: [PATCH 1/6] backend-common: provide legacyPlugin shims for identity and tokenManager Signed-off-by: Patrik Oldsberg --- packages/backend-common/api-report.md | 32 ++++- .../backend-common/src/compat/legacy/index.ts | 2 +- .../src/compat/legacy/legacy.ts | 134 +++++++++++++++--- 3 files changed, 143 insertions(+), 25 deletions(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 61efc0f3a1..85885eeabe 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: ServerTokenManager; + identity: LegacyIdentityService; + } >; }>, ) => BackendFeatureCompat; @@ -384,7 +403,12 @@ export function makeLegacyPlugin< ): ( name: string, createRouterImport: Promise<{ - default: LegacyCreateRouter>; + default: LegacyCreateRouter< + TransformedEnv & { + tokenManager: ServerTokenManager; + identity: LegacyIdentityService; + } + >; }>, ) => BackendFeatureCompat; 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.ts b/packages/backend-common/src/compat/legacy/legacy.ts index 4dc7d1a09a..380c64ce0f 100644 --- a/packages/backend-common/src/compat/legacy/legacy.ts +++ b/packages/backend-common/src/compat/legacy/legacy.ts @@ -18,13 +18,17 @@ import { AuthService, coreServices, createBackendPlugin, - HttpRouterService, + HttpAuthService, + 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 +50,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 +73,64 @@ function wrapTokenManager(tokenManager: TokenManager, auth: AuthService) { } await tokenManager.authenticate(token); }, - } satisfies TokenManager; + }; +} + +/** + * Originally IdentityApi from `@backstage/plugin-auth-node`, not 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 + >; +} + +function createIdentityServiceShim( + httpAuth: HttpAuthService, + userInfo: UserInfoService, +): LegacyIdentityService { + return { + async getIdentity(options) { + const credentials = await httpAuth + .credentials(options.request, { + allow: ['user'], + }) + .catch(() => undefined); + + if (!credentials) { + return undefined; + } + + const info = await userInfo.getUserInfo(credentials); + + return { + get token(): string { + throw new Error( + 'The identity service shim provided by legacyPlugin does not support accessing the user token. ' + + 'The calling plugin needs to be migrated to use the new auth services. ' + + 'See https://backstage.io/docs/tutorials/auth-service-migration', + ); + }, + identity: { + type: 'user', + userEntityRef: info.userEntityRef, + ownershipEntityRefs: info.ownershipEntityRefs, + }, + }; + }, + }; } /** @@ -87,7 +153,12 @@ export function makeLegacyPlugin< return ( name: string, createRouterImport: Promise<{ - default: LegacyCreateRouter>; + default: LegacyCreateRouter< + TransformedEnv & { + tokenManager: ServerTokenManager; + identity: LegacyIdentityService; + } + >; }>, ) => { const compatPlugin = createBackendPlugin({ @@ -96,10 +167,22 @@ export function makeLegacyPlugin< env.registerInit({ deps: { ...envMapping, - _router: coreServices.httpRouter, - _auth: coreServices.auth, + $$router: coreServices.httpRouter, + $$auth: coreServices.auth, + $$httpAuth: coreServices.httpAuth, + $$userInfo: coreServices.userInfo, + $$config: coreServices.rootConfig, + $$logger: coreServices.logger, }, - async init({ _router, _auth, ...envDeps }) { + async init({ + $$auth, + $$config, + $$httpAuth, + $$logger, + $$router, + $$userInfo, + ...envDeps + }) { const { default: createRouter } = await createRouterImport; const pluginEnv = Object.fromEntries( Object.entries(envDeps).map(([key, dep]) => { @@ -107,19 +190,32 @@ 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 httpAuth = $$httpAuth as typeof coreServices.httpAuth.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(httpAuth, userInfo); + + const pluginRouter = await createRouter( + pluginEnv as TransformedEnv & { + tokenManager: ServerTokenManager; + identity: LegacyIdentityService; + }, + ); + router.use(pluginRouter); }, }); }, @@ -155,9 +251,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), From d1608fe7ca252a177a710147d86f6960619f9509 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 21 Aug 2024 01:32:47 +0200 Subject: [PATCH 2/6] backend-common: make token accessbile for legacy shim for identity service Signed-off-by: Patrik Oldsberg --- .../src/compat/legacy/legacy.ts | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/backend-common/src/compat/legacy/legacy.ts b/packages/backend-common/src/compat/legacy/legacy.ts index 380c64ce0f..1ca8fb7482 100644 --- a/packages/backend-common/src/compat/legacy/legacy.ts +++ b/packages/backend-common/src/compat/legacy/legacy.ts @@ -18,7 +18,6 @@ import { AuthService, coreServices, createBackendPlugin, - HttpAuthService, LoggerService, RootConfigService, ServiceRef, @@ -97,32 +96,32 @@ export interface LegacyIdentityService { >; } +// This doesn't use DefaultIdentityClient because we will be removing it and break support for ownershipEntityRefs function createIdentityServiceShim( - httpAuth: HttpAuthService, + auth: AuthService, userInfo: UserInfoService, ): LegacyIdentityService { return { async getIdentity(options) { - const credentials = await httpAuth - .credentials(options.request, { - allow: ['user'], - }) - .catch(() => undefined); + const authHeader = options.request.headers.authorization; + if (typeof authHeader !== 'string') { + return undefined; + } - if (!credentials) { + 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 { - get token(): string { - throw new Error( - 'The identity service shim provided by legacyPlugin does not support accessing the user token. ' + - 'The calling plugin needs to be migrated to use the new auth services. ' + - 'See https://backstage.io/docs/tutorials/auth-service-migration', - ); - }, + token, identity: { type: 'user', userEntityRef: info.userEntityRef, @@ -169,7 +168,6 @@ export function makeLegacyPlugin< ...envMapping, $$router: coreServices.httpRouter, $$auth: coreServices.auth, - $$httpAuth: coreServices.httpAuth, $$userInfo: coreServices.userInfo, $$config: coreServices.rootConfig, $$logger: coreServices.logger, @@ -177,7 +175,6 @@ export function makeLegacyPlugin< async init({ $$auth, $$config, - $$httpAuth, $$logger, $$router, $$userInfo, @@ -196,7 +193,6 @@ export function makeLegacyPlugin< const auth = $$auth as typeof coreServices.auth.T; const config = $$config as typeof coreServices.rootConfig.T; - const httpAuth = $$httpAuth as typeof coreServices.httpAuth.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; @@ -207,7 +203,7 @@ export function makeLegacyPlugin< config, logger, ); - pluginEnv.identity = createIdentityServiceShim(httpAuth, userInfo); + pluginEnv.identity = createIdentityServiceShim(auth, userInfo); const pluginRouter = await createRouter( pluginEnv as TransformedEnv & { From 8ba77edaabc41767d19c15fb986db0fd84fd7b60 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 21 Aug 2024 01:34:01 +0200 Subject: [PATCH 3/6] changesets: changeset for legacyPlugin service shims Signed-off-by: Patrik Oldsberg --- .changeset/lucky-sheep-cover.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lucky-sheep-cover.md diff --git a/.changeset/lucky-sheep-cover.md b/.changeset/lucky-sheep-cover.md new file mode 100644 index 0000000000..ea54a14b8c --- /dev/null +++ b/.changeset/lucky-sheep-cover.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +The `legacyPlugin` and `makeLeagcyPlugin` 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. From 52bad657d511ac25a06615bba7bc89ae586fce24 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 21 Aug 2024 10:50:42 +0200 Subject: [PATCH 4/6] backend-common: add test for identity service shim Signed-off-by: Patrik Oldsberg --- .../src/compat/legacy/legacy.test.ts | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/src/compat/legacy/legacy.test.ts b/packages/backend-common/src/compat/legacy/legacy.test.ts index 08a645f660..d052b500b8 100644 --- a/packages/backend-common/src/compat/legacy/legacy.test.ts +++ b/packages/backend-common/src/compat/legacy/legacy.test.ts @@ -18,9 +18,14 @@ 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 @@ -115,4 +120,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], + }, + }); + }); }); From 336b307557727950211815ecf063488bc901f002 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 21 Aug 2024 10:51:14 +0200 Subject: [PATCH 5/6] backend-common: remove token manager implementation from shim test Signed-off-by: Patrik Oldsberg --- packages/backend-common/src/compat/legacy/legacy.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/backend-common/src/compat/legacy/legacy.test.ts b/packages/backend-common/src/compat/legacy/legacy.test.ts index d052b500b8..b7a5ff1a78 100644 --- a/packages/backend-common/src/compat/legacy/legacy.test.ts +++ b/packages/backend-common/src/compat/legacy/legacy.test.ts @@ -30,8 +30,6 @@ 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 () => { @@ -46,7 +44,6 @@ describe('legacyPlugin', () => { await startTestBackend({ features: [ authServiceFactory, - tokenManagerServiceFactory, mockServices.rootConfig.factory({ data: { backend: { From 5e64df0b22fb5441bd36bfd0e181fac3bf6545c2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 21 Aug 2024 13:27:20 +0200 Subject: [PATCH 6/6] backend-common: review fixes for service shims Signed-off-by: Patrik Oldsberg --- .changeset/lucky-sheep-cover.md | 2 +- packages/backend-common/api-report.md | 4 ++-- packages/backend-common/src/compat/legacy/legacy.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.changeset/lucky-sheep-cover.md b/.changeset/lucky-sheep-cover.md index ea54a14b8c..9b9420a73a 100644 --- a/.changeset/lucky-sheep-cover.md +++ b/.changeset/lucky-sheep-cover.md @@ -2,4 +2,4 @@ '@backstage/backend-common': patch --- -The `legacyPlugin` and `makeLeagcyPlugin` 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. +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 85885eeabe..b1d752cef2 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -362,7 +362,7 @@ export const legacyPlugin: ( }; } > & { - tokenManager: ServerTokenManager; + tokenManager: TokenManager; identity: LegacyIdentityService; } >; @@ -405,7 +405,7 @@ export function makeLegacyPlugin< createRouterImport: Promise<{ default: LegacyCreateRouter< TransformedEnv & { - tokenManager: ServerTokenManager; + tokenManager: TokenManager; identity: LegacyIdentityService; } >; diff --git a/packages/backend-common/src/compat/legacy/legacy.ts b/packages/backend-common/src/compat/legacy/legacy.ts index 1ca8fb7482..dc4ddbcdf0 100644 --- a/packages/backend-common/src/compat/legacy/legacy.ts +++ b/packages/backend-common/src/compat/legacy/legacy.ts @@ -76,7 +76,7 @@ function createTokenManagerShim( } /** - * Originally IdentityApi from `@backstage/plugin-auth-node`, not re-declared here for backwards compatibility + * Originally IdentityApi from `@backstage/plugin-auth-node`, re-declared here for backwards compatibility * @public * @deprecated Only relevant for legacy plugins, which are deprecated. */ @@ -154,7 +154,7 @@ export function makeLegacyPlugin< createRouterImport: Promise<{ default: LegacyCreateRouter< TransformedEnv & { - tokenManager: ServerTokenManager; + tokenManager: TokenManager; identity: LegacyIdentityService; } >; @@ -207,7 +207,7 @@ export function makeLegacyPlugin< const pluginRouter = await createRouter( pluginEnv as TransformedEnv & { - tokenManager: ServerTokenManager; + tokenManager: TokenManager; identity: LegacyIdentityService; }, );