diff --git a/src/lib/middleware/api-token-middleware.test.ts b/src/lib/middleware/api-token-middleware.test.ts index d5ecb12d2a..1be03a31e6 100644 --- a/src/lib/middleware/api-token-middleware.test.ts +++ b/src/lib/middleware/api-token-middleware.test.ts @@ -65,6 +65,7 @@ test('should add user if known token', async () => { project: ALL, environment: ALL, type: ApiTokenType.CLIENT, + secret: 'a', }); const apiTokenService = { getUserForToken: jest.fn().mockReturnValue(apiUser), @@ -96,6 +97,7 @@ test('should not add user if not /api/client', async () => { project: ALL, environment: ALL, type: ApiTokenType.CLIENT, + secret: 'a', }); const apiTokenService = { @@ -134,6 +136,7 @@ test('should not add user if disabled', async () => { project: ALL, environment: ALL, type: ApiTokenType.CLIENT, + secret: 'a', }); const apiTokenService = { getUserForToken: jest.fn().mockReturnValue(apiUser), diff --git a/src/lib/middleware/api-token-middleware.ts b/src/lib/middleware/api-token-middleware.ts index 8c2aab9ab2..5f5a71a2cd 100644 --- a/src/lib/middleware/api-token-middleware.ts +++ b/src/lib/middleware/api-token-middleware.ts @@ -6,8 +6,12 @@ const isClientApi = ({ path }) => { return path && path.startsWith('/api/client'); }; +const isProxyApi = ({ path }) => { + return path && path.startsWith('/api/frontend'); +}; + export const TOKEN_TYPE_ERROR_MESSAGE = - 'invalid token: expected an admin token but got a client token instead'; + 'invalid token: expected a different token type for this endpoint'; const apiAccessMiddleware = ( { @@ -31,9 +35,13 @@ const apiAccessMiddleware = ( try { const apiToken = req.header('authorization'); const apiUser = apiTokenService.getUserForToken(apiToken); + const { CLIENT, PROXY } = ApiTokenType; if (apiUser) { - if (apiUser.type === ApiTokenType.CLIENT && !isClientApi(req)) { + if ( + (apiUser.type === CLIENT && !isClientApi(req)) || + (apiUser.type === PROXY && !isProxyApi(req)) + ) { res.status(403).send({ message: TOKEN_TYPE_ERROR_MESSAGE }); return; } diff --git a/src/lib/middleware/demo-authentication.ts b/src/lib/middleware/demo-authentication.ts index 2151fcd549..c988705764 100644 --- a/src/lib/middleware/demo-authentication.ts +++ b/src/lib/middleware/demo-authentication.ts @@ -47,6 +47,7 @@ function demoAuthentication( environment: 'default', type: ApiTokenType.CLIENT, project: '*', + secret: 'a', }); } next(); diff --git a/src/lib/middleware/rbac-middleware.test.ts b/src/lib/middleware/rbac-middleware.test.ts index 66c5916384..b07b9ef229 100644 --- a/src/lib/middleware/rbac-middleware.test.ts +++ b/src/lib/middleware/rbac-middleware.test.ts @@ -50,6 +50,7 @@ test('should give api-user ADMIN permission', async () => { project: '*', environment: '*', type: ApiTokenType.ADMIN, + secret: 'a', }), }; @@ -75,6 +76,7 @@ test('should not give api-user ADMIN permission', async () => { project: '*', environment: '*', type: ApiTokenType.CLIENT, + secret: 'a', }), }; diff --git a/src/lib/services/api-token-service.ts b/src/lib/services/api-token-service.ts index 3916d5201e..42680b2806 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 } from '../types/permissions'; +import { ADMIN, CLIENT, PROXY } from '../types/permissions'; import { IUnleashStores } from '../types/stores'; import { IUnleashConfig } from '../types/option'; import ApiUser from '../types/api-user'; @@ -20,6 +20,22 @@ import BadDataError from '../error/bad-data-error'; import { minutesToMilliseconds } from 'date-fns'; import { IEnvironmentStore } from 'lib/types/stores/environment-store'; +const resolveTokenPermissions = (tokenType: string) => { + if (tokenType === ApiTokenType.ADMIN) { + return [ADMIN]; + } + + if (tokenType === ApiTokenType.CLIENT) { + return [CLIENT]; + } + + if (tokenType === ApiTokenType.PROXY) { + return [PROXY]; + } + + return []; +}; + export class ApiTokenService { private store: IApiTokenStore; @@ -88,15 +104,13 @@ export class ApiTokenService { public getUserForToken(secret: string): ApiUser | undefined { const token = this.activeTokens.find((t) => t.secret === secret); if (token) { - const permissions = - token.type === ApiTokenType.ADMIN ? [ADMIN] : [CLIENT]; - return new ApiUser({ username: token.username, - permissions, + permissions: resolveTokenPermissions(token.type), projects: token.projects, environment: token.environment, type: token.type, + secret: token.secret, }); } return undefined; diff --git a/src/lib/types/api-user.ts b/src/lib/types/api-user.ts index 8514edd6d0..dfd325feb7 100644 --- a/src/lib/types/api-user.ts +++ b/src/lib/types/api-user.ts @@ -8,6 +8,7 @@ interface IApiUserData { project?: string; environment: string; type: ApiTokenType; + secret: string; } export default class ApiUser { @@ -23,6 +24,8 @@ export default class ApiUser { readonly type: ApiTokenType; + readonly secret: string; + constructor({ username, permissions = [CLIENT], @@ -30,6 +33,7 @@ export default class ApiUser { project, environment, type, + secret, }: IApiUserData) { if (!username) { throw new TypeError('username is required'); @@ -38,6 +42,7 @@ export default class ApiUser { this.permissions = permissions; this.environment = environment; this.type = type; + this.secret = secret; if (projects && projects.length > 0) { this.projects = projects; } else { diff --git a/src/lib/types/models/api-token.ts b/src/lib/types/models/api-token.ts index b31948248e..88132e3331 100644 --- a/src/lib/types/models/api-token.ts +++ b/src/lib/types/models/api-token.ts @@ -6,6 +6,7 @@ export const ALL = '*'; export enum ApiTokenType { CLIENT = 'client', ADMIN = 'admin', + PROXY = 'proxy', } export interface ILegacyApiTokenCreate { @@ -102,6 +103,12 @@ export const validateApiToken = ({ 'Client token cannot be scoped to all environments', ); } + + if (type === ApiTokenType.PROXY && environment === ALL) { + throw new BadDataError( + 'Proxy token cannot be scoped to all environments', + ); + } }; export const validateApiTokenEnvironment = ( diff --git a/src/lib/types/permissions.ts b/src/lib/types/permissions.ts index 10486656a2..2bbea67da1 100644 --- a/src/lib/types/permissions.ts +++ b/src/lib/types/permissions.ts @@ -1,6 +1,7 @@ //Special export const ADMIN = 'ADMIN'; export const CLIENT = 'CLIENT'; +export const PROXY = 'PROXY'; export const NONE = 'NONE'; export const CREATE_FEATURE = 'CREATE_FEATURE'; diff --git a/src/test/e2e/api/admin/project/features.e2e.test.ts b/src/test/e2e/api/admin/project/features.e2e.test.ts index e0d1f5ea0b..02b9960396 100644 --- a/src/test/e2e/api/admin/project/features.e2e.test.ts +++ b/src/test/e2e/api/admin/project/features.e2e.test.ts @@ -1862,6 +1862,7 @@ test('Should not allow changing project to target project without the same enabl project: '*', type: ApiTokenType.ADMIN, environment: '*', + secret: 'a', }); await expect(async () => app.services.projectService.changeProject( @@ -1945,6 +1946,7 @@ test('Should allow changing project to target project with the same enabled envi project: '*', type: ApiTokenType.ADMIN, environment: '*', + secret: 'a', }); await expect(async () => app.services.projectService.changeProject( 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 f787fea168..e1ee33c1a6 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 @@ -230,7 +230,7 @@ Object { "type": "string", }, "type": Object { - "description": "client, admin.", + "description": "client, admin, proxy.", "type": "string", }, "username": Object { @@ -667,7 +667,7 @@ Object { "type": "string", }, "type": Object { - "description": "client, admin.", + "description": "client, admin, proxy.", "type": "string", }, "username": Object {