diff --git a/plugins/user-settings-backend/README.md b/plugins/user-settings-backend/README.md index 05a8c15c6d..5a98c86326 100644 --- a/plugins/user-settings-backend/README.md +++ b/plugins/user-settings-backend/README.md @@ -18,64 +18,47 @@ yarn --cwd packages/backend add @backstage/plugin-user-settings-backend ```ts // packages/backend/src/plugins/userSettings.ts -import { IdentityClient } from '@backstage/plugin-auth-node'; -import { - createRouter, - createUserSettingsStore, -} from '@backstage/plugin-user-settings-backend'; -import { Router } from 'express'; - +import { createRouter } from '@backstage/plugin-user-settings-backend'; import { PluginEnvironment } from '../types'; -export default async function createPlugin({ - database, - discovery, -}: PluginEnvironment): Promise { - const identity = IdentityClient.create({ - discovery, - issuer: await discovery.getExternalBaseUrl('auth'), - }); - +export default async function createPlugin(env: PluginEnvironment) { return await createRouter({ - userSettingsStore: await createUserSettingsStore(database), - identity, + database: env.database, + identity: env.identity, }); } ``` -3. Add the new routes to your backend by modifying the - `packages/backend/src/index.ts`: +3. Add the new routes to your backend by modifying `packages/backend/src/index.ts`: ```diff -// packages/backend/src/index.ts -+ import userSettings from './plugins/userSettings'; -async function main() { -+ const userSettingsEnv = useHotMemoize(module, () => -+ createEnv('userSettings'), -+ ); - const apiRouter = Router(); -+ apiRouter.use('/user-settings', await userSettings(userSettingsEnv)); + // packages/backend/src/index.ts ++import userSettings from './plugins/userSettings'; + async function main() { ++ const userSettingsEnv = useHotMemoize(module, () => createEnv('userSettings')); + const apiRouter = Router(); ++ apiRouter.use('/user-settings', await userSettings(userSettingsEnv)); } ``` ## Setup app To make use of the user settings backend, replace the `WebStorage` with the -`PersistentStorage` by using the `storageApiRef`. +`UserSettingsStorage` by using the `storageApiRef`. ```diff -// packages/app/src/apis.ts -import { - AnyApiFactory, - createApiFactory, + // packages/app/src/apis.ts + import { + AnyApiFactory, + createApiFactory, + discoveryApiRef, + fetchApiRef - errorApiRef, + errorApiRef, + storageApiRef, -} from '@backstage/core-plugin-api'; -+ import { PersistentStorage } from '@backstage/core-app-api'; + } from '@backstage/core-plugin-api'; ++import { UserSettingsStorage } from '@backstage/core-app-api'; -export const apis: AnyApiFactory[] = [ + export const apis: AnyApiFactory[] = [ + createApiFactory({ + api: storageApiRef, + deps: { @@ -84,9 +67,9 @@ export const apis: AnyApiFactory[] = [ + fetchApi: fetchApiRef, + }, + factory: ({ discoveryApi, errorApi, fetchApi }) => -+ PersistentStorage.create({ discoveryApi, errorApi, fetchApi }), ++ UserSettingsStorage.create({ discoveryApi, errorApi, fetchApi }), + }), -]; + ]; ``` ## Development diff --git a/plugins/user-settings-backend/api-report.md b/plugins/user-settings-backend/api-report.md index 15e8b5eb5b..1bc37bbec7 100644 --- a/plugins/user-settings-backend/api-report.md +++ b/plugins/user-settings-backend/api-report.md @@ -4,7 +4,7 @@ ```ts import express from 'express'; -import { IdentityClient } from '@backstage/plugin-auth-node'; +import { IdentityApi } from '@backstage/plugin-auth-node'; import { PluginDatabaseManager } from '@backstage/backend-common'; // @public @@ -15,7 +15,7 @@ export interface RouterOptions { // (undocumented) database: PluginDatabaseManager; // (undocumented) - identity: IdentityClient; + identity: IdentityApi; } // (No @packageDocumentation comment for this package) diff --git a/plugins/user-settings-backend/src/service/router.test.ts b/plugins/user-settings-backend/src/service/router.test.ts index c8df77a845..64cdfcc465 100644 --- a/plugins/user-settings-backend/src/service/router.test.ts +++ b/plugins/user-settings-backend/src/service/router.test.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { IdentityClient } from '@backstage/plugin-auth-node'; +import { + BackstageIdentityResponse, + IdentityApi, +} from '@backstage/plugin-auth-node'; import express from 'express'; import request from 'supertest'; import { UserSettingsStore } from '../database/UserSettingsStore'; @@ -26,9 +29,12 @@ describe('createRouter', () => { set: jest.fn(), delete: jest.fn(), }; - const authenticateMock = jest.fn(); - const identityClient: jest.Mocked> = { - authenticate: authenticateMock, + const getIdentityMock = jest.fn< + Promise, + any + >(); + const identityApi: jest.Mocked> = { + getIdentity: getIdentityMock, }; let app: express.Express; @@ -36,7 +42,7 @@ describe('createRouter', () => { beforeEach(async () => { const router = await createRouterInternal({ userSettingsStore, - identity: identityClient as IdentityClient, + identity: identityApi as IdentityApi, }); app = express().use(router); @@ -49,8 +55,13 @@ describe('createRouter', () => { describe('GET /buckets/:bucket/keys/:key', () => { it('returns ok', async () => { const setting = { bucket: 'my-bucket', key: 'my-key', value: 'a' }; - authenticateMock.mockResolvedValue({ - identity: { userEntityRef: 'user-1' }, + getIdentityMock.mockResolvedValue({ + token: 'a', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'user-1', + }, }); userSettingsStore.get.mockResolvedValue(setting); @@ -62,7 +73,7 @@ describe('createRouter', () => { expect(responses.status).toEqual(200); expect(responses.body).toEqual(setting); - expect(authenticateMock).toHaveBeenCalledWith('foo'); + expect(getIdentityMock).toHaveBeenCalledTimes(1); expect(userSettingsStore.get).toHaveBeenCalledTimes(1); expect(userSettingsStore.get).toHaveBeenCalledWith({ userEntityRef: 'user-1', @@ -83,8 +94,13 @@ describe('createRouter', () => { describe('DELETE /buckets/:bucket/keys/:key', () => { it('returns ok', async () => { - authenticateMock.mockResolvedValue({ - identity: { userEntityRef: 'user-1' }, + getIdentityMock.mockResolvedValue({ + token: 'a', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'user-1', + }, }); userSettingsStore.delete.mockResolvedValue(); @@ -95,7 +111,7 @@ describe('createRouter', () => { expect(responses.status).toEqual(204); - expect(authenticateMock).toHaveBeenCalledWith('foo'); + expect(getIdentityMock).toHaveBeenCalledTimes(1); expect(userSettingsStore.delete).toHaveBeenCalledTimes(1); expect(userSettingsStore.delete).toHaveBeenCalledWith({ userEntityRef: 'user-1', @@ -117,8 +133,13 @@ describe('createRouter', () => { describe('PUT /buckets/:bucket/keys/:key', () => { it('returns ok', async () => { const setting = { bucket: 'my-bucket', key: 'my-key', value: 'a' }; - authenticateMock.mockResolvedValue({ - identity: { userEntityRef: 'user-1' }, + getIdentityMock.mockResolvedValue({ + token: 'a', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'user-1', + }, }); userSettingsStore.set.mockResolvedValue(); @@ -132,7 +153,7 @@ describe('createRouter', () => { expect(responses.status).toEqual(200); expect(responses.body).toEqual(setting); - expect(authenticateMock).toHaveBeenCalledWith('foo'); + expect(getIdentityMock).toHaveBeenCalledTimes(1); expect(userSettingsStore.set).toHaveBeenCalledTimes(1); expect(userSettingsStore.set).toHaveBeenCalledWith({ userEntityRef: 'user-1', @@ -149,8 +170,13 @@ describe('createRouter', () => { }); it('returns an error if the value is not given', async () => { - authenticateMock.mockResolvedValue({ - identity: { userEntityRef: 'user-1' }, + getIdentityMock.mockResolvedValue({ + token: 'a', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'user-1', + }, }); const responses = await request(app) @@ -160,7 +186,7 @@ describe('createRouter', () => { expect(responses.status).toEqual(400); - expect(authenticateMock).toHaveBeenCalledWith('foo'); + expect(getIdentityMock).toHaveBeenCalledTimes(1); expect(userSettingsStore.set).not.toHaveBeenCalled(); }); diff --git a/plugins/user-settings-backend/src/service/router.ts b/plugins/user-settings-backend/src/service/router.ts index 487a6b4fee..6792042585 100644 --- a/plugins/user-settings-backend/src/service/router.ts +++ b/plugins/user-settings-backend/src/service/router.ts @@ -16,10 +16,7 @@ import { errorHandler, PluginDatabaseManager } from '@backstage/backend-common'; import { AuthenticationError, InputError } from '@backstage/errors'; -import { - getBearerTokenFromAuthorizationHeader, - IdentityClient, -} from '@backstage/plugin-auth-node'; +import { IdentityApi } from '@backstage/plugin-auth-node'; import express, { Request } from 'express'; import Router from 'express-promise-router'; import { DatabaseUserSettingsStore } from '../database/DatabaseUserSettingsStore'; @@ -30,7 +27,7 @@ import { UserSettingsStore } from '../database/UserSettingsStore'; */ export interface RouterOptions { database: PluginDatabaseManager; - identity: IdentityClient; + identity: IdentityApi; } /** @@ -52,7 +49,7 @@ export async function createRouter( } export async function createRouterInternal(options: { - identity: IdentityClient; + identity: IdentityApi; userSettingsStore: UserSettingsStore; }): Promise { const router = Router(); @@ -62,18 +59,13 @@ export async function createRouterInternal(options: { * Helper method to extract the userEntityRef from the request. */ const getUserEntityRef = async (req: Request): Promise => { - const token = getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ); - - if (!token) { + // throws an AuthenticationError in case the token exists but is invalid + const identity = await options.identity.getIdentity({ request: req }); + if (!identity) { throw new AuthenticationError(`Missing token in 'authorization' header`); } - // throws an AuthenticationError in case the token is invalid - const user = await options.identity.authenticate(token); - - return user.identity.userEntityRef; + return identity.identity.userEntityRef; }; // get a single value diff --git a/plugins/user-settings-backend/src/service/standaloneServer.ts b/plugins/user-settings-backend/src/service/standaloneServer.ts index 28af3a163f..6dd8c7f72d 100644 --- a/plugins/user-settings-backend/src/service/standaloneServer.ts +++ b/plugins/user-settings-backend/src/service/standaloneServer.ts @@ -15,8 +15,7 @@ */ import { createServiceBuilder, useHotMemoize } from '@backstage/backend-common'; -import { IdentityClient } from '@backstage/plugin-auth-node'; -import { Router } from 'express'; +import { IdentityApi } from '@backstage/plugin-auth-node'; import { Server } from 'http'; import Knex from 'knex'; import { Logger } from 'winston'; @@ -44,11 +43,19 @@ export async function startStandaloneServer( logger.debug('Starting application server...'); - const identityMock = { - authenticate: async token => ({ - identity: { userEntityRef: token ?? 'user:default/john_doe' }, - }), - } as IdentityClient; + const identityMock: IdentityApi = { + async getIdentity({ request }) { + const token = request.headers.authorization?.split(' ')[1]; + return { + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: token || 'user:default/john_doe', + }, + token: token || 'no-token', + }; + }, + }; const router = await createRouterInternal({ userSettingsStore: await DatabaseUserSettingsStore.create({ @@ -57,18 +64,9 @@ export async function startStandaloneServer( identity: identityMock, }); - // set a custom authorization header to simplify the development - const authWrapper = Router(); - authWrapper.use((req, _res, next) => { - req.headers.authorization = - req.headers.authorization ?? 'Bearer user:default/john_doe'; - next(); - }); - authWrapper.use(router); - let service = createServiceBuilder(module) .setPort(options.port) - .addRouter('/user-settings', authWrapper); + .addRouter('/user-settings', router); if (options.enableCors) { service = service.enableCors({ origin: 'http://localhost:3000' });