Merge pull request #26115 from backstage/rugvip/shim

backend-common: add shims for token manager and identity services
This commit is contained in:
Patrik Oldsberg
2024-08-21 15:10:26 +02:00
committed by GitHub
5 changed files with 200 additions and 29 deletions
+5
View File
@@ -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.
+28 -4
View File
@@ -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<TEnv> = (deps: TEnv) => Promise<RequestHandler>;
// @public @deprecated
export interface LegacyIdentityService {
// (undocumented)
getIdentity(options: { request: Request_2<unknown> }): 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<TransformedEnv<TEnv, TEnvTransforms>>;
default: LegacyCreateRouter<
TransformedEnv<TEnv, TEnvTransforms> & {
tokenManager: TokenManager;
identity: LegacyIdentityService;
}
>;
}>,
) => BackendFeature;
@@ -15,4 +15,4 @@
*/
export { legacyPlugin, makeLegacyPlugin } from './legacy';
export type { LegacyCreateRouter } from './legacy';
export type { LegacyCreateRouter, LegacyIdentityService } from './legacy';
@@ -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],
},
});
});
});
@@ -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<unknown> }): 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<TransformedEnv<TEnv, TEnvTransforms>>;
default: LegacyCreateRouter<
TransformedEnv<TEnv, TEnvTransforms> & {
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<TEnv, TEnvTransforms>,
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<TEnv, TEnvTransforms> & {
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),