use the IdentityApi properly

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2022-09-07 11:53:53 +02:00
parent 9018da59a9
commit 0f88eab1b7
5 changed files with 89 additions and 90 deletions
+22 -39
View File
@@ -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<Router> {
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
+2 -2
View File
@@ -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)
@@ -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<Partial<IdentityClient>> = {
authenticate: authenticateMock,
const getIdentityMock = jest.fn<
Promise<BackstageIdentityResponse | undefined>,
any
>();
const identityApi: jest.Mocked<Partial<IdentityApi>> = {
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();
});
@@ -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<express.Router> {
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<string> => {
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
@@ -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' });