diff --git a/src/lib/middleware/api-token-middleware.ts b/src/lib/middleware/api-token-middleware.ts index 10740c149f..32cb948476 100644 --- a/src/lib/middleware/api-token-middleware.ts +++ b/src/lib/middleware/api-token-middleware.ts @@ -36,13 +36,13 @@ const apiAccessMiddleware = ( try { const apiToken = req.header('authorization'); const apiUser = apiTokenService.getUserForToken(apiToken); - const { CLIENT, PROXY } = ApiTokenType; + const { CLIENT, FRONTEND } = ApiTokenType; if (apiUser) { if ( (apiUser.type === CLIENT && !isClientApi(req)) || - (apiUser.type === PROXY && !isProxyApi(req)) || - (apiUser.type === PROXY && !experimental.embedProxy) + (apiUser.type === FRONTEND && !isProxyApi(req)) || + (apiUser.type === FRONTEND && !experimental.embedProxy) ) { res.status(403).send({ message: TOKEN_TYPE_ERROR_MESSAGE }); return; diff --git a/src/lib/schema/api-token-schema.test.ts b/src/lib/schema/api-token-schema.test.ts index a35feb6b21..5e74a42eba 100644 --- a/src/lib/schema/api-token-schema.test.ts +++ b/src/lib/schema/api-token-schema.test.ts @@ -56,10 +56,10 @@ test('should set metadata', async () => { expect(token.projects).toBeUndefined(); }); -test('should allow for embedded proxy (frontend) key', async () => { +test('should allow for frontend key (embedded proxy)', async () => { let token = await createApiToken.validateAsync({ username: 'test', - type: 'proxy', + type: 'frontend', project: 'default', metadata: { corsOrigins: ['*'], @@ -68,10 +68,10 @@ test('should allow for embedded proxy (frontend) key', async () => { expect(token.error).toBeUndefined(); }); -test('should set environment to default for proxy key', async () => { +test('should set environment to default for frontend key', async () => { let token = await createApiToken.validateAsync({ username: 'test', - type: 'proxy', + type: 'frontend', project: 'default', metadata: { corsOrigins: ['*'], diff --git a/src/lib/schema/api-token-schema.ts b/src/lib/schema/api-token-schema.ts index c1a678322c..7b9c69bb64 100644 --- a/src/lib/schema/api-token-schema.ts +++ b/src/lib/schema/api-token-schema.ts @@ -10,7 +10,11 @@ export const createApiToken = joi .string() .lowercase() .required() - .valid(ApiTokenType.ADMIN, ApiTokenType.CLIENT, ApiTokenType.PROXY), + .valid( + ApiTokenType.ADMIN, + ApiTokenType.CLIENT, + ApiTokenType.FRONTEND, + ), expiresAt: joi.date().optional(), project: joi.when('projects', { not: joi.required(), @@ -18,7 +22,7 @@ export const createApiToken = joi }), projects: joi.array().min(0).optional(), environment: joi.when('type', { - is: joi.string().valid(ApiTokenType.CLIENT, ApiTokenType.PROXY), + is: joi.string().valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND), then: joi.string().optional().default(DEFAULT_ENV), otherwise: joi.string().optional().default(ALL), }), diff --git a/src/lib/services/api-token-service.test.ts b/src/lib/services/api-token-service.test.ts index 162e091f6a..6e7478b1f7 100644 --- a/src/lib/services/api-token-service.test.ts +++ b/src/lib/services/api-token-service.test.ts @@ -1,7 +1,7 @@ import { ApiTokenService } from './api-token-service'; import { createTestConfig } from '../../test/config/test-config'; import { IUnleashConfig } from '../server-impl'; -import { ApiTokenType } from '../types/models/api-token'; +import { ApiTokenType, IApiTokenCreate } from '../types/models/api-token'; import FakeApiTokenStore from '../../test/fixtures/fake-api-token-store'; import FakeEnvironmentStore from '../../test/fixtures/fake-environment-store'; @@ -33,3 +33,37 @@ test('Should init api token', async () => { expect(tokens).toHaveLength(1); }); + +test("Shouldn't return frontend token when secret is undefined", async () => { + const token: IApiTokenCreate = { + environment: 'default', + projects: ['*'], + secret: '*:*:some-random-string', + type: ApiTokenType.FRONTEND, + username: 'front', + expiresAt: null, + }; + + const config: IUnleashConfig = createTestConfig({}); + const apiTokenStore = new FakeApiTokenStore(); + const environmentStore = new FakeEnvironmentStore(); + + await environmentStore.create({ + name: 'default', + enabled: true, + protected: true, + type: 'test', + sortOrder: 1, + }); + + const apiTokenService = new ApiTokenService( + { apiTokenStore, environmentStore }, + config, + ); + + await apiTokenService.createApiTokenWithProjects(token); + await apiTokenService.fetchActiveTokens(); + + expect(apiTokenService.getUserForToken(undefined)).toEqual(undefined); + expect(apiTokenService.getUserForToken('')).toEqual(undefined); +}); diff --git a/src/lib/services/api-token-service.ts b/src/lib/services/api-token-service.ts index 8d8dc9ee16..1517d2dbf5 100644 --- a/src/lib/services/api-token-service.ts +++ b/src/lib/services/api-token-service.ts @@ -1,6 +1,6 @@ import crypto from 'crypto'; import { Logger } from '../logger'; -import { ADMIN, CLIENT, PROXY } from '../types/permissions'; +import { ADMIN, CLIENT, FRONTEND } from '../types/permissions'; import { IUnleashStores } from '../types/stores'; import { IUnleashConfig } from '../types/option'; import ApiUser from '../types/api-user'; @@ -29,8 +29,8 @@ const resolveTokenPermissions = (tokenType: string) => { return [CLIENT]; } - if (tokenType === ApiTokenType.PROXY) { - return [PROXY]; + if (tokenType === ApiTokenType.FRONTEND) { + return [FRONTEND]; } return []; @@ -69,7 +69,7 @@ export class ApiTokenService { } } - private async fetchActiveTokens(): Promise { + async fetchActiveTokens(): Promise { try { this.activeTokens = await this.getAllActiveTokens(); } finally { @@ -102,12 +102,18 @@ export class ApiTokenService { } public getUserForToken(secret: string): ApiUser | undefined { + if (!secret) { + return undefined; + } + 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 && secret) { + token = this.activeTokens.find( + (t) => t.metadata.alias && t.metadata.alias === secret, + ); } if (token) { diff --git a/src/lib/services/proxy-service.ts b/src/lib/services/proxy-service.ts index f28ebc7436..1172c663f5 100644 --- a/src/lib/services/proxy-service.ts +++ b/src/lib/services/proxy-service.ts @@ -115,6 +115,6 @@ export class ProxyService { } private static assertExpectedTokenType({ type }: ApiUser) { - assert(type === ApiTokenType.PROXY || type === ApiTokenType.ADMIN); + assert(type === ApiTokenType.FRONTEND || type === ApiTokenType.ADMIN); } } diff --git a/src/lib/types/models/api-token.ts b/src/lib/types/models/api-token.ts index 03c70f6845..9e35d31d00 100644 --- a/src/lib/types/models/api-token.ts +++ b/src/lib/types/models/api-token.ts @@ -6,7 +6,7 @@ export const ALL = '*'; export enum ApiTokenType { CLIENT = 'client', ADMIN = 'admin', - PROXY = 'proxy', + FRONTEND = 'frontend', } export interface ILegacyApiTokenCreate { @@ -108,9 +108,9 @@ export const validateApiToken = ({ ); } - if (type === ApiTokenType.PROXY && environment === ALL) { + if (type === ApiTokenType.FRONTEND && environment === ALL) { throw new BadDataError( - 'Proxy token cannot be scoped to all environments', + 'Frontend token cannot be scoped to all environments', ); } }; diff --git a/src/lib/types/permissions.ts b/src/lib/types/permissions.ts index 2bbea67da1..24c471ac9c 100644 --- a/src/lib/types/permissions.ts +++ b/src/lib/types/permissions.ts @@ -1,7 +1,7 @@ //Special export const ADMIN = 'ADMIN'; export const CLIENT = 'CLIENT'; -export const PROXY = 'PROXY'; +export const FRONTEND = 'FRONTEND'; export const NONE = 'NONE'; export const CREATE_FEATURE = 'CREATE_FEATURE'; 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 27cf67d447..0fb1d9381e 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 @@ -246,7 +246,7 @@ Object { "type": "string", }, "type": Object { - "description": "client, admin, proxy.", + "description": "client, admin, frontend.", "type": "string", }, "username": Object { @@ -683,7 +683,7 @@ Object { "type": "string", }, "type": Object { - "description": "client, admin, proxy.", + "description": "client, admin, frontend.", "type": "string", }, "username": Object { diff --git a/src/test/e2e/api/proxy/proxy.e2e.test.ts b/src/test/e2e/api/proxy/proxy.e2e.test.ts index 9a53b34a2d..8e2252fe26 100644 --- a/src/test/e2e/api/proxy/proxy.e2e.test.ts +++ b/src/test/e2e/api/proxy/proxy.e2e.test.ts @@ -88,7 +88,7 @@ const createProject = async (id: string): Promise => { await app.services.projectService.createProject({ id, name: id }, user); }; -test('should require a proxy token or an admin token', async () => { +test('should require a frontend token or an admin token', async () => { await app.request .get('/api/frontend') .expect('Content-Type', /json/) @@ -117,64 +117,64 @@ test('should allow requests with an admin token', async () => { .expect((res) => expect(res.body).toEqual({ toggles: [] })); }); -test('should not allow admin requests with a proxy token', async () => { - const proxyToken = await createApiToken(ApiTokenType.PROXY); +test('should not allow admin requests with a frontend token', async () => { + const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await app.request .get('/api/admin/features') - .set('Authorization', proxyToken.secret) + .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(403); }); -test('should not allow client requests with a proxy token', async () => { - const proxyToken = await createApiToken(ApiTokenType.PROXY); +test('should not allow client requests with a frontend token', async () => { + const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await app.request .get('/api/client/features') - .set('Authorization', proxyToken.secret) + .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(403); }); -test('should not allow requests with an invalid proxy token', async () => { - const proxyToken = await createApiToken(ApiTokenType.PROXY); +test('should not allow requests with an invalid frontend token', async () => { + const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await app.request .get('/api/frontend') - .set('Authorization', proxyToken.secret.slice(0, -1)) + .set('Authorization', frontendToken.secret.slice(0, -1)) .expect('Content-Type', /json/) .expect(401); }); -test('should allow requests with a proxy token', async () => { - const proxyToken = await createApiToken(ApiTokenType.PROXY); +test('should allow requests with a frontend token', async () => { + const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await app.request .get('/api/frontend') - .set('Authorization', proxyToken.secret) + .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => expect(res.body).toEqual({ toggles: [] })); }); test('should return 405 from unimplemented endpoints', async () => { - const proxyToken = await createApiToken(ApiTokenType.PROXY); + const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await app.request .post('/api/frontend') .send({}) - .set('Authorization', proxyToken.secret) + .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(405); await app.request .get('/api/frontend/client/features') - .set('Authorization', proxyToken.secret) + .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(405); await app.request .get('/api/frontend/health') - .set('Authorization', proxyToken.secret) + .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(405); await app.request .get('/api/frontend/internal-backstage/prometheus') - .set('Authorization', proxyToken.secret) + .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(405); }); @@ -183,16 +183,16 @@ test('should return 405 from unimplemented endpoints', async () => { test.todo('should enforce token CORS settings'); test('should accept client registration requests', async () => { - const proxyToken = await createApiToken(ApiTokenType.PROXY); + const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await app.request .post('/api/frontend/client/register') - .set('Authorization', proxyToken.secret) + .set('Authorization', frontendToken.secret) .send({}) .expect('Content-Type', /json/) .expect(400); await app.request .post('/api/frontend/client/register') - .set('Authorization', proxyToken.secret) + .set('Authorization', frontendToken.secret) .send({ appName: randomId(), instanceId: randomId(), @@ -211,7 +211,7 @@ test('should store proxy client metrics', async () => { const appName = randomId(); const instanceId = randomId(); const featureName = randomId(); - const proxyToken = await createApiToken(ApiTokenType.PROXY); + const frontendToken = await createApiToken(ApiTokenType.FRONTEND); const adminToken = await createApiToken(ApiTokenType.ADMIN, { projects: ['*'], environment: '*', @@ -232,7 +232,7 @@ test('should store proxy client metrics', async () => { }); await app.request .post('/api/frontend/client/metrics') - .set('Authorization', proxyToken.secret) + .set('Authorization', frontendToken.secret) .send({ appName, instanceId, @@ -246,7 +246,7 @@ test('should store proxy client metrics', async () => { .expect((res) => expect(res.text).toEqual('OK')); await app.request .post('/api/frontend/client/metrics') - .set('Authorization', proxyToken.secret) + .set('Authorization', frontendToken.secret) .send({ appName, instanceId, @@ -282,7 +282,7 @@ test('should store proxy client metrics', async () => { }); test('should filter features by enabled/disabled', async () => { - const proxyToken = await createApiToken(ApiTokenType.PROXY); + const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await createFeatureToggle({ name: 'enabledFeature1', enabled: true, @@ -300,7 +300,7 @@ test('should filter features by enabled/disabled', async () => { }); await app.request .get('/api/frontend') - .set('Authorization', proxyToken.secret) + .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { @@ -324,7 +324,7 @@ test('should filter features by enabled/disabled', async () => { }); test('should filter features by strategies', async () => { - const proxyToken = await createApiToken(ApiTokenType.PROXY); + const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await createFeatureToggle({ name: 'featureWithoutStrategies', enabled: false, @@ -345,7 +345,7 @@ test('should filter features by strategies', async () => { }); await app.request .get('/api/frontend') - .set('Authorization', proxyToken.secret) + .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { @@ -363,7 +363,7 @@ test('should filter features by strategies', async () => { }); test('should filter features by constraints', async () => { - const proxyToken = await createApiToken(ApiTokenType.PROXY); + const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await createFeatureToggle({ name: 'featureWithAppNameA', enabled: true, @@ -396,19 +396,19 @@ test('should filter features by constraints', async () => { }); await app.request .get('/api/frontend?appName=a') - .set('Authorization', proxyToken.secret) + .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => expect(res.body.toggles).toHaveLength(2)); await app.request .get('/api/frontend?appName=b') - .set('Authorization', proxyToken.secret) + .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => expect(res.body.toggles).toHaveLength(1)); await app.request .get('/api/frontend?appName=c') - .set('Authorization', proxyToken.secret) + .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => expect(res.body.toggles).toHaveLength(0)); @@ -419,11 +419,11 @@ test('should filter features by project', async () => { const projectB = 'projectB'; await createProject(projectA); await createProject(projectB); - const proxyTokenDefault = await createApiToken(ApiTokenType.PROXY); - const proxyTokenProjectA = await createApiToken(ApiTokenType.PROXY, { + const frontendTokenDefault = await createApiToken(ApiTokenType.FRONTEND); + const frontendTokenProjectA = await createApiToken(ApiTokenType.FRONTEND, { projects: [projectA], }); - const proxyTokenProjectAB = await createApiToken(ApiTokenType.PROXY, { + const frontendTokenProjectAB = await createApiToken(ApiTokenType.FRONTEND, { projects: [projectA, projectB], }); await createFeatureToggle({ @@ -445,7 +445,7 @@ test('should filter features by project', async () => { }); await app.request .get('/api/frontend') - .set('Authorization', proxyTokenDefault.secret) + .set('Authorization', frontendTokenDefault.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { @@ -462,7 +462,7 @@ test('should filter features by project', async () => { }); await app.request .get('/api/frontend') - .set('Authorization', proxyTokenProjectA.secret) + .set('Authorization', frontendTokenProjectA.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { @@ -479,7 +479,7 @@ test('should filter features by project', async () => { }); await app.request .get('/api/frontend') - .set('Authorization', proxyTokenProjectAB.secret) + .set('Authorization', frontendTokenProjectAB.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { @@ -521,15 +521,21 @@ test('should filter features by environment', async () => { environmentB, 'default', ); - const proxyTokenEnvironmentDefault = await createApiToken( - ApiTokenType.PROXY, + const frontendTokenEnvironmentDefault = await createApiToken( + ApiTokenType.FRONTEND, + ); + const frontendTokenEnvironmentA = await createApiToken( + ApiTokenType.FRONTEND, + { + environment: environmentA, + }, + ); + const frontendTokenEnvironmentB = await createApiToken( + ApiTokenType.FRONTEND, + { + environment: environmentB, + }, ); - const proxyTokenEnvironmentA = await createApiToken(ApiTokenType.PROXY, { - environment: environmentA, - }); - const proxyTokenEnvironmentB = await createApiToken(ApiTokenType.PROXY, { - environment: environmentB, - }); await createFeatureToggle({ name: 'featureInEnvironmentDefault', enabled: true, @@ -549,7 +555,7 @@ test('should filter features by environment', async () => { }); await app.request .get('/api/frontend') - .set('Authorization', proxyTokenEnvironmentDefault.secret) + .set('Authorization', frontendTokenEnvironmentDefault.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { @@ -566,7 +572,7 @@ test('should filter features by environment', async () => { }); await app.request .get('/api/frontend') - .set('Authorization', proxyTokenEnvironmentA.secret) + .set('Authorization', frontendTokenEnvironmentA.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { @@ -583,7 +589,7 @@ test('should filter features by environment', async () => { }); await app.request .get('/api/frontend') - .set('Authorization', proxyTokenEnvironmentB.secret) + .set('Authorization', frontendTokenEnvironmentB.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { diff --git a/src/test/fixtures/fake-api-token-store.ts b/src/test/fixtures/fake-api-token-store.ts index 1a4410c575..cddc36eb3d 100644 --- a/src/test/fixtures/fake-api-token-store.ts +++ b/src/test/fixtures/fake-api-token-store.ts @@ -44,7 +44,9 @@ export default class FakeApiTokenStore } async getAllActive(): Promise { - return this.tokens.filter((token) => token.expiresAt > new Date()); + return this.tokens.filter( + (token) => token.expiresAt === null || token.expiresAt > new Date(), + ); } async insert(newToken: IApiTokenCreate): Promise {