From 9676165de9b4d5f8f06914c748421b0364cb7539 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 17 Aug 2022 10:55:52 +0200 Subject: [PATCH] Feat: Create frontend API key (#1932) * refactor: remove unused API definition routes * feat: embed proxy endpoints * feat: add metadata * feat: update schema * feat: check token metadata for alias if none is found * refactor: add api token metadata test coverage * refactor: proxy key validation and default values * refactor: update snapshot Co-authored-by: olav Co-authored-by: Fredrik Oseberg --- src/lib/db/api-token-store.ts | 4 ++ .../api-token-schema.test.ts.snap | 17 +++++ src/lib/openapi/spec/api-token-schema.test.ts | 62 +++++++++++++++---- src/lib/openapi/spec/api-token-schema.ts | 16 +++++ src/lib/schema/api-token-schema.test.ts | 37 +++++++++++ src/lib/schema/api-token-schema.ts | 4 +- src/lib/services/api-token-service.ts | 9 ++- src/lib/types/models/api-token.ts | 4 ++ .../__snapshots__/openapi.e2e.test.ts.snap | 16 +++++ src/test/fixtures/fake-api-token-store.ts | 1 + 10 files changed, 156 insertions(+), 14 deletions(-) diff --git a/src/lib/db/api-token-store.ts b/src/lib/db/api-token-store.ts index 47f35f145d..16d5162330 100644 --- a/src/lib/db/api-token-store.ts +++ b/src/lib/db/api-token-store.ts @@ -45,6 +45,7 @@ const tokenRowReducer = (acc, tokenRow) => { environment: token.environment ? token.environment : ALL, expiresAt: token.expires_at, createdAt: token.created_at, + metadata: token.metadata, }; } const currentToken = acc[tokenRow.secret]; @@ -65,6 +66,7 @@ const toRow = (newToken: IApiTokenCreate) => ({ environment: newToken.environment === ALL ? undefined : newToken.environment, expires_at: newToken.expiresAt, + metadata: newToken.metadata || {}, }); const toTokens = (rows: any[]): IApiToken[] => { @@ -124,6 +126,7 @@ export class ApiTokenStore implements IApiTokenStore { 'type', 'expires_at', 'created_at', + 'metadata', 'seen_at', 'environment', 'token_project_link.project', @@ -150,6 +153,7 @@ export class ApiTokenStore implements IApiTokenStore { await Promise.all(updateProjectTasks); return { ...newToken, + metadata: newToken.metadata || {}, project: newToken.projects?.join(',') || '*', createdAt: row.created_at, }; diff --git a/src/lib/openapi/spec/__snapshots__/api-token-schema.test.ts.snap b/src/lib/openapi/spec/__snapshots__/api-token-schema.test.ts.snap index 7a3c062650..4e9be0e49d 100644 --- a/src/lib/openapi/spec/__snapshots__/api-token-schema.test.ts.snap +++ b/src/lib/openapi/spec/__snapshots__/api-token-schema.test.ts.snap @@ -16,3 +16,20 @@ Object { "schema": "#/components/schemas/apiTokenSchema", } `; + +exports[`apiTokenSchema metadata - does not allow custom metadata parameters 1`] = ` +Object { + "errors": Array [ + Object { + "instancePath": "/metadata", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": Object { + "additionalProperty": "arbitraryParameter", + }, + "schemaPath": "#/properties/metadata/additionalProperties", + }, + ], + "schema": "#/components/schemas/apiTokenSchema", +} +`; diff --git a/src/lib/openapi/spec/api-token-schema.test.ts b/src/lib/openapi/spec/api-token-schema.test.ts index d4baecf491..7a5efc269f 100644 --- a/src/lib/openapi/spec/api-token-schema.test.ts +++ b/src/lib/openapi/spec/api-token-schema.test.ts @@ -2,24 +2,64 @@ import { ApiTokenType } from '../../types/models/api-token'; import { validateSchema } from '../validate'; import { ApiTokenSchema } from './api-token-schema'; +const defaultData: ApiTokenSchema = { + secret: '', + username: '', + type: ApiTokenType.CLIENT, + environment: '', + projects: [], + expiresAt: '2022-01-01T00:00:00.000Z', + createdAt: '2022-01-01T00:00:00.000Z', + seenAt: '2022-01-01T00:00:00.000Z', + project: '', +}; + test('apiTokenSchema', () => { - const data: ApiTokenSchema = { - secret: '', - username: '', - type: ApiTokenType.CLIENT, - environment: '', - projects: [], - expiresAt: '2022-01-01T00:00:00.000Z', - createdAt: '2022-01-01T00:00:00.000Z', - seenAt: '2022-01-01T00:00:00.000Z', - project: '', - }; + const data: ApiTokenSchema = { ...defaultData }; expect( validateSchema('#/components/schemas/apiTokenSchema', data), ).toBeUndefined(); }); +test('apiTokenSchema metadata - should allow empty object', () => { + const data: ApiTokenSchema = { ...defaultData, metadata: {} }; + + expect( + validateSchema('#/components/schemas/apiTokenSchema', data), + ).toBeUndefined(); +}); + +test('apiTokenSchema metadata - allows for corsOrigins and/or alias', () => { + expect( + validateSchema('#/components/schemas/apiTokenSchema', { + ...defaultData, + metadata: { corsOrigins: ['*'] }, + }), + ).toBeUndefined(); + expect( + validateSchema('#/components/schemas/apiTokenSchema', { + ...defaultData, + metadata: { alias: 'secret' }, + }), + ).toBeUndefined(); + expect( + validateSchema('#/components/schemas/apiTokenSchema', { + ...defaultData, + metadata: { corsOrigins: ['*'], alias: 'abc' }, + }), + ).toBeUndefined(); +}); + +test('apiTokenSchema metadata - does not allow custom metadata parameters', () => { + expect( + validateSchema('#/components/schemas/apiTokenSchema', { + ...defaultData, + metadata: { arbitraryParameter: true }, + }), + ).toMatchSnapshot(); +}); + test('apiTokenSchema empty', () => { expect( validateSchema('#/components/schemas/apiTokenSchema', {}), diff --git a/src/lib/openapi/spec/api-token-schema.ts b/src/lib/openapi/spec/api-token-schema.ts index 8b48aba4b0..cda8528f71 100644 --- a/src/lib/openapi/spec/api-token-schema.ts +++ b/src/lib/openapi/spec/api-token-schema.ts @@ -44,6 +44,22 @@ export const apiTokenSchema = { format: 'date-time', nullable: true, }, + metadata: { + type: 'object', + additionalProperties: false, + properties: { + corsOrigins: { + type: 'array', + items: { + type: 'string', + }, + }, + alias: { + type: 'string', + }, + }, + nullable: true, + }, }, components: {}, } as const; diff --git a/src/lib/schema/api-token-schema.test.ts b/src/lib/schema/api-token-schema.test.ts index cdd272ec51..a35feb6b21 100644 --- a/src/lib/schema/api-token-schema.test.ts +++ b/src/lib/schema/api-token-schema.test.ts @@ -42,3 +42,40 @@ test('should not have projects set if project is present', async () => { }); expect(token.projects).not.toBeDefined(); }); + +test('should set metadata', async () => { + let token = await createApiToken.validateAsync({ + username: 'test', + type: 'admin', + project: 'default', + metadata: { + corsOrigins: ['*'], + alias: 'secret', + }, + }); + expect(token.projects).toBeUndefined(); +}); + +test('should allow for embedded proxy (frontend) key', async () => { + let token = await createApiToken.validateAsync({ + username: 'test', + type: 'proxy', + project: 'default', + metadata: { + corsOrigins: ['*'], + }, + }); + expect(token.error).toBeUndefined(); +}); + +test('should set environment to default for proxy key', async () => { + let token = await createApiToken.validateAsync({ + username: 'test', + type: 'proxy', + project: 'default', + metadata: { + corsOrigins: ['*'], + }, + }); + expect(token.environment).toEqual('default'); +}); diff --git a/src/lib/schema/api-token-schema.ts b/src/lib/schema/api-token-schema.ts index c6725f67a2..c1a678322c 100644 --- a/src/lib/schema/api-token-schema.ts +++ b/src/lib/schema/api-token-schema.ts @@ -10,7 +10,7 @@ export const createApiToken = joi .string() .lowercase() .required() - .valid(ApiTokenType.ADMIN, ApiTokenType.CLIENT), + .valid(ApiTokenType.ADMIN, ApiTokenType.CLIENT, ApiTokenType.PROXY), expiresAt: joi.date().optional(), project: joi.when('projects', { not: joi.required(), @@ -18,7 +18,7 @@ export const createApiToken = joi }), projects: joi.array().min(0).optional(), environment: joi.when('type', { - is: joi.string().valid(ApiTokenType.CLIENT), + is: joi.string().valid(ApiTokenType.CLIENT, ApiTokenType.PROXY), then: joi.string().optional().default(DEFAULT_ENV), otherwise: joi.string().optional().default(ALL), }), diff --git a/src/lib/services/api-token-service.ts b/src/lib/services/api-token-service.ts index 42680b2806..8d8dc9ee16 100644 --- a/src/lib/services/api-token-service.ts +++ b/src/lib/services/api-token-service.ts @@ -102,7 +102,14 @@ export class ApiTokenService { } public getUserForToken(secret: string): ApiUser | undefined { - const token = this.activeTokens.find((t) => t.secret === secret); + let token = this.activeTokens.find((t) => t.secret === secret); + + // If the token is not found, try to find it in the legacy format with the metadata alias + // This is to ensure that previous proxies we set up for our customers continue working + if (!token) { + token = this.activeTokens.find((t) => t.metadata.alias === secret); + } + if (token) { return new ApiUser({ username: token.username, diff --git a/src/lib/types/models/api-token.ts b/src/lib/types/models/api-token.ts index 88132e3331..03c70f6845 100644 --- a/src/lib/types/models/api-token.ts +++ b/src/lib/types/models/api-token.ts @@ -22,17 +22,21 @@ export interface ILegacyApiTokenCreate { export interface IApiTokenCreate { secret: string; username: string; + metadata?: Metadata; type: ApiTokenType; environment: string; projects: string[]; expiresAt?: Date; } +type Metadata = { [key: string]: unknown }; + export interface IApiToken extends IApiTokenCreate { createdAt: Date; seenAt?: Date; environment: string; project: string; + metadata: Metadata; } export const isAllProjects = (projects: string[]): boolean => { diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index de549f36df..27cf67d447 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -212,6 +212,22 @@ Object { "nullable": true, "type": "string", }, + "metadata": Object { + "additionalProperties": false, + "nullable": true, + "properties": Object { + "alias": Object { + "type": "string", + }, + "corsOrigins": Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + }, + "type": "object", + }, "project": Object { "type": "string", }, diff --git a/src/test/fixtures/fake-api-token-store.ts b/src/test/fixtures/fake-api-token-store.ts index d1d24274ad..1a4410c575 100644 --- a/src/test/fixtures/fake-api-token-store.ts +++ b/src/test/fixtures/fake-api-token-store.ts @@ -52,6 +52,7 @@ export default class FakeApiTokenStore createdAt: new Date(), project: newToken.projects?.join(',') || '*', ...newToken, + metadata: {}, }; this.tokens.push(apiToken); this.emit('insert');