diff --git a/src/lib/openapi/index.ts b/src/lib/openapi/index.ts index 86d04fca28..b13142a4a5 100644 --- a/src/lib/openapi/index.ts +++ b/src/lib/openapi/index.ts @@ -111,6 +111,8 @@ import { proxyFeatureSchema } from './spec/proxy-feature-schema'; import { proxyClientSchema } from './spec/proxy-client-schema'; import { proxyMetricsSchema } from './spec/proxy-metrics-schema'; import { setUiConfigSchema } from './spec/set-ui-config-schema'; +import { edgeTokenSchema } from './spec/edge-token-schema'; +import { validateEdgeTokensSchema } from './spec/validate-edge-tokens-schema'; // All schemas in `openapi/spec` should be listed here. export const schemas = { @@ -221,6 +223,8 @@ export const schemas = { proxyFeaturesSchema, proxyFeatureSchema, proxyMetricsSchema, + edgeTokenSchema, + validateEdgeTokensSchema, }; // Schemas must have an $id property on the form "#/components/schemas/mySchema". diff --git a/src/lib/openapi/spec/edge-token-schema.ts b/src/lib/openapi/spec/edge-token-schema.ts new file mode 100644 index 0000000000..cfac9e6d0f --- /dev/null +++ b/src/lib/openapi/spec/edge-token-schema.ts @@ -0,0 +1,27 @@ +import { FromSchema } from 'json-schema-to-ts'; +import { ApiTokenType } from '../../types/models/api-token'; + +export const edgeTokenSchema = { + $id: '#/components/schemas/edgeTokenSchema', + type: 'object', + additionalProperties: false, + required: ['token', 'projects', 'type'], + properties: { + projects: { + type: 'array', + items: { + type: 'string', + }, + }, + type: { + type: 'string', + enum: Object.values(ApiTokenType), + }, + token: { + type: 'string', + }, + }, + components: {}, +} as const; + +export type EdgeTokenSchema = FromSchema; diff --git a/src/lib/openapi/spec/validate-edge-tokens-schema.ts b/src/lib/openapi/spec/validate-edge-tokens-schema.ts new file mode 100644 index 0000000000..8c77d0a270 --- /dev/null +++ b/src/lib/openapi/spec/validate-edge-tokens-schema.ts @@ -0,0 +1,27 @@ +import { FromSchema } from 'json-schema-to-ts'; +import { edgeTokenSchema } from './edge-token-schema'; + +export const validateEdgeTokensSchema = { + $id: '#/components/schemas/validateEdgeTokensSchema', + type: 'object', + additionalProperties: false, + required: ['tokens'], + properties: { + tokens: { + type: 'array', + anyOf: [ + { items: { $ref: '#/components/schemas/edgeTokenSchema' } }, + { items: { type: 'string' } }, + ], + }, + }, + components: { + schemas: { + edgeTokenSchema, + }, + }, +} as const; + +export type ValidateEdgeTokensSchema = FromSchema< + typeof validateEdgeTokensSchema +>; diff --git a/src/lib/openapi/util/openapi-tags.ts b/src/lib/openapi/util/openapi-tags.ts index 238c7d85d9..dd4b5a6584 100644 --- a/src/lib/openapi/util/openapi-tags.ts +++ b/src/lib/openapi/util/openapi-tags.ts @@ -77,6 +77,7 @@ const OPENAPI_TAGS = [ description: 'Experimental endpoints that may change or disappear at any time.', }, + { name: 'Edge', description: 'Endpoints related to Unleash on the Edge.' }, ] as const; // make the export mutable, so it can be used in a schema diff --git a/src/lib/routes/edge-api/index.ts b/src/lib/routes/edge-api/index.ts new file mode 100644 index 0000000000..8926f50174 --- /dev/null +++ b/src/lib/routes/edge-api/index.ts @@ -0,0 +1,69 @@ +import { Response } from 'express'; +import Controller from '../controller'; +import { IUnleashConfig, IUnleashServices } from '../../types'; +import { Logger } from '../../logger'; +import { NONE } from '../../types/permissions'; +import { createResponseSchema } from '../../openapi/util/create-response-schema'; +import { RequestBody } from '../unleash-types'; +import { createRequestSchema } from '../../openapi/util/create-request-schema'; +import { + validateEdgeTokensSchema, + ValidateEdgeTokensSchema, +} from '../../openapi/spec/validate-edge-tokens-schema'; +import EdgeService from '../../services/edge-service'; +import { OpenApiService } from '../../services/openapi-service'; + +export default class EdgeController extends Controller { + private readonly logger: Logger; + + private edgeService: EdgeService; + + private openApiService: OpenApiService; + + constructor( + config: IUnleashConfig, + { + edgeService, + openApiService, + }: Pick, + ) { + super(config); + this.logger = config.getLogger('edge-api/index.ts'); + this.edgeService = edgeService; + this.openApiService = openApiService; + + this.route({ + method: 'post', + path: '/validate', + handler: this.getValidTokens, + permission: NONE, + middleware: [ + this.openApiService.validPath({ + tags: ['Edge'], + operationId: 'getValidTokens', + requestBody: createRequestSchema( + 'validateEdgeTokensSchema', + ), + responses: { + 200: createResponseSchema('validateEdgeTokensSchema'), + }, + }), + ], + }); + } + + async getValidTokens( + req: RequestBody, + res: Response, + ): Promise { + const tokens = await this.edgeService.getValidTokens( + req.body.tokens as string[], + ); + this.openApiService.respondWithValidation( + 200, + res, + validateEdgeTokensSchema.$id, + tokens, + ); + } +} diff --git a/src/lib/routes/index.ts b/src/lib/routes/index.ts index 132b909401..076ee8fc33 100644 --- a/src/lib/routes/index.ts +++ b/src/lib/routes/index.ts @@ -11,6 +11,7 @@ const Controller = require('./controller'); import { HealthCheckController } from './health-check'; import ProxyController from './proxy-api'; import { conditionalMiddleware } from '../middleware/conditional-middleware'; +import EdgeController from './edge-api'; class IndexRouter extends Controller { constructor(config: IUnleashConfig, services: IUnleashServices) { @@ -37,6 +38,8 @@ class IndexRouter extends Controller { new ProxyController(config, services).router, ), ); + + this.use('/edge', new EdgeController(config, services).router); } } diff --git a/src/lib/routes/unleash-types.ts b/src/lib/routes/unleash-types.ts index a7238188f3..7f6af2131f 100644 --- a/src/lib/routes/unleash-types.ts +++ b/src/lib/routes/unleash-types.ts @@ -11,3 +11,7 @@ export interface IAuthRequest< logout: () => void; session: any; } + +export interface RequestBody extends Express.Request { + body: T; +} diff --git a/src/lib/services/edge-service.ts b/src/lib/services/edge-service.ts new file mode 100644 index 0000000000..8206fbb4ee --- /dev/null +++ b/src/lib/services/edge-service.ts @@ -0,0 +1,40 @@ +import { IUnleashStores, IUnleashConfig } from '../types'; +import { Logger } from '../logger'; +import { IApiTokenStore } from '../types/stores/api-token-store'; +import { EdgeTokenSchema } from '../openapi/spec/edge-token-schema'; +import { constantTimeCompare } from '../util/constantTimeCompare'; +import { ValidateEdgeTokensSchema } from '../openapi/spec/validate-edge-tokens-schema'; + +export default class EdgeService { + private logger: Logger; + + private apiTokenStore: IApiTokenStore; + + constructor( + { apiTokenStore }: Pick, + { getLogger }: Pick, + ) { + this.logger = getLogger('lib/services/edge-service.ts'); + this.apiTokenStore = apiTokenStore; + } + + async getValidTokens(tokens: string[]): Promise { + const activeTokens = await this.apiTokenStore.getAllActive(); + const edgeTokens = tokens.reduce((result: EdgeTokenSchema[], token) => { + const dbToken = activeTokens.find((activeToken) => + constantTimeCompare(activeToken.secret, token), + ); + if (dbToken) { + result.push({ + token: token, + type: dbToken.type, + projects: dbToken.projects, + }); + } + return result; + }, []); + return { tokens: edgeTokens }; + } +} + +module.exports = EdgeService; diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index a43fd85c50..b200ec96f5 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -34,6 +34,7 @@ import { ClientSpecService } from './client-spec-service'; import { PlaygroundService } from './playground-service'; import { GroupService } from './group-service'; import { ProxyService } from './proxy-service'; +import EdgeService from './edge-service'; export const createServices = ( stores: IUnleashStores, config: IUnleashConfig, @@ -98,6 +99,8 @@ export const createServices = ( segmentService, }); + const edgeService = new EdgeService(stores, config); + return { accessService, addonService, @@ -132,6 +135,7 @@ export const createServices = ( playgroundService, groupService, proxyService, + edgeService, }; }; diff --git a/src/lib/types/services.ts b/src/lib/types/services.ts index c5b45d61ff..2bbe32ea49 100644 --- a/src/lib/types/services.ts +++ b/src/lib/types/services.ts @@ -30,6 +30,7 @@ import { ClientSpecService } from '../services/client-spec-service'; import { PlaygroundService } from 'lib/services/playground-service'; import { GroupService } from '../services/group-service'; import { ProxyService } from '../services/proxy-service'; +import EdgeService from '../services/edge-service'; export interface IUnleashServices { accessService: AccessService; @@ -65,4 +66,5 @@ export interface IUnleashServices { clientSpecService: ClientSpecService; playgroundService: PlaygroundService; proxyService: ProxyService; + edgeService: EdgeService; } 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 f0b27924cd..078bbcd273 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 @@ -758,6 +758,34 @@ Object { }, ], }, + "edgeTokenSchema": Object { + "additionalProperties": false, + "properties": Object { + "projects": Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + "token": Object { + "type": "string", + }, + "type": Object { + "enum": Array [ + "client", + "admin", + "frontend", + ], + "type": "string", + }, + }, + "required": Array [ + "token", + "projects", + "type", + ], + "type": "object", + }, "emailSchema": Object { "additionalProperties": false, "properties": Object { @@ -3133,6 +3161,30 @@ Object { }, "type": "array", }, + "validateEdgeTokensSchema": Object { + "additionalProperties": false, + "properties": Object { + "tokens": Object { + "anyOf": Array [ + Object { + "items": Object { + "$ref": "#/components/schemas/edgeTokenSchema", + }, + }, + Object { + "items": Object { + "type": "string", + }, + }, + ], + "type": "array", + }, + }, + "required": Array [ + "tokens", + ], + "type": "object", + }, "validatePasswordSchema": Object { "additionalProperties": false, "properties": Object { @@ -6837,6 +6889,37 @@ If the provided project does not exist, the list of events will be empty.", ], }, }, + "/edge/validate": Object { + "post": Object { + "operationId": "getValidTokens", + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/validateEdgeTokensSchema", + }, + }, + }, + "description": "validateEdgeTokensSchema", + "required": true, + }, + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/validateEdgeTokensSchema", + }, + }, + }, + "description": "validateEdgeTokensSchema", + }, + }, + "tags": Array [ + "Edge", + ], + }, + }, "/health": Object { "get": Object { "operationId": "getHealth", @@ -6907,6 +6990,10 @@ If the provided project does not exist, the list of events will be empty.", "description": "Create, update, and delete [context fields](https://docs.getunleash.io/user_guide/unleash_context) that Unleash is aware of.", "name": "Context", }, + Object { + "description": "Endpoints related to Unleash on the Edge.", + "name": "Edge", + }, Object { "description": "Create, update, delete, enable or disable [environments](https://docs.getunleash.io/user_guide/environments) for this Unleash instance.", "name": "Environments",