1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-10 17:53:36 +02:00

feat: add support for proxy keys

This commit is contained in:
olav 2022-08-15 15:56:12 +02:00
parent 1ba5701422
commit 37ad0a3799
10 changed files with 52 additions and 9 deletions

View File

@ -65,6 +65,7 @@ test('should add user if known token', async () => {
project: ALL, project: ALL,
environment: ALL, environment: ALL,
type: ApiTokenType.CLIENT, type: ApiTokenType.CLIENT,
secret: 'a',
}); });
const apiTokenService = { const apiTokenService = {
getUserForToken: jest.fn().mockReturnValue(apiUser), getUserForToken: jest.fn().mockReturnValue(apiUser),
@ -96,6 +97,7 @@ test('should not add user if not /api/client', async () => {
project: ALL, project: ALL,
environment: ALL, environment: ALL,
type: ApiTokenType.CLIENT, type: ApiTokenType.CLIENT,
secret: 'a',
}); });
const apiTokenService = { const apiTokenService = {
@ -134,6 +136,7 @@ test('should not add user if disabled', async () => {
project: ALL, project: ALL,
environment: ALL, environment: ALL,
type: ApiTokenType.CLIENT, type: ApiTokenType.CLIENT,
secret: 'a',
}); });
const apiTokenService = { const apiTokenService = {
getUserForToken: jest.fn().mockReturnValue(apiUser), getUserForToken: jest.fn().mockReturnValue(apiUser),

View File

@ -6,8 +6,12 @@ const isClientApi = ({ path }) => {
return path && path.startsWith('/api/client'); return path && path.startsWith('/api/client');
}; };
const isProxyApi = ({ path }) => {
return path && path.startsWith('/api/frontend');
};
export const TOKEN_TYPE_ERROR_MESSAGE = 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 = ( const apiAccessMiddleware = (
{ {
@ -31,9 +35,13 @@ const apiAccessMiddleware = (
try { try {
const apiToken = req.header('authorization'); const apiToken = req.header('authorization');
const apiUser = apiTokenService.getUserForToken(apiToken); const apiUser = apiTokenService.getUserForToken(apiToken);
const { CLIENT, PROXY } = ApiTokenType;
if (apiUser) { 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 }); res.status(403).send({ message: TOKEN_TYPE_ERROR_MESSAGE });
return; return;
} }

View File

@ -47,6 +47,7 @@ function demoAuthentication(
environment: 'default', environment: 'default',
type: ApiTokenType.CLIENT, type: ApiTokenType.CLIENT,
project: '*', project: '*',
secret: 'a',
}); });
} }
next(); next();

View File

@ -50,6 +50,7 @@ test('should give api-user ADMIN permission', async () => {
project: '*', project: '*',
environment: '*', environment: '*',
type: ApiTokenType.ADMIN, type: ApiTokenType.ADMIN,
secret: 'a',
}), }),
}; };
@ -75,6 +76,7 @@ test('should not give api-user ADMIN permission', async () => {
project: '*', project: '*',
environment: '*', environment: '*',
type: ApiTokenType.CLIENT, type: ApiTokenType.CLIENT,
secret: 'a',
}), }),
}; };

View File

@ -1,6 +1,6 @@
import crypto from 'crypto'; import crypto from 'crypto';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { ADMIN, CLIENT } from '../types/permissions'; import { ADMIN, CLIENT, PROXY } from '../types/permissions';
import { IUnleashStores } from '../types/stores'; import { IUnleashStores } from '../types/stores';
import { IUnleashConfig } from '../types/option'; import { IUnleashConfig } from '../types/option';
import ApiUser from '../types/api-user'; import ApiUser from '../types/api-user';
@ -20,6 +20,22 @@ import BadDataError from '../error/bad-data-error';
import { minutesToMilliseconds } from 'date-fns'; import { minutesToMilliseconds } from 'date-fns';
import { IEnvironmentStore } from 'lib/types/stores/environment-store'; 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 { export class ApiTokenService {
private store: IApiTokenStore; private store: IApiTokenStore;
@ -88,15 +104,13 @@ export class ApiTokenService {
public getUserForToken(secret: string): ApiUser | undefined { public getUserForToken(secret: string): ApiUser | undefined {
const token = this.activeTokens.find((t) => t.secret === secret); const token = this.activeTokens.find((t) => t.secret === secret);
if (token) { if (token) {
const permissions =
token.type === ApiTokenType.ADMIN ? [ADMIN] : [CLIENT];
return new ApiUser({ return new ApiUser({
username: token.username, username: token.username,
permissions, permissions: resolveTokenPermissions(token.type),
projects: token.projects, projects: token.projects,
environment: token.environment, environment: token.environment,
type: token.type, type: token.type,
secret: token.secret,
}); });
} }
return undefined; return undefined;

View File

@ -8,6 +8,7 @@ interface IApiUserData {
project?: string; project?: string;
environment: string; environment: string;
type: ApiTokenType; type: ApiTokenType;
secret: string;
} }
export default class ApiUser { export default class ApiUser {
@ -23,6 +24,8 @@ export default class ApiUser {
readonly type: ApiTokenType; readonly type: ApiTokenType;
readonly secret: string;
constructor({ constructor({
username, username,
permissions = [CLIENT], permissions = [CLIENT],
@ -30,6 +33,7 @@ export default class ApiUser {
project, project,
environment, environment,
type, type,
secret,
}: IApiUserData) { }: IApiUserData) {
if (!username) { if (!username) {
throw new TypeError('username is required'); throw new TypeError('username is required');
@ -38,6 +42,7 @@ export default class ApiUser {
this.permissions = permissions; this.permissions = permissions;
this.environment = environment; this.environment = environment;
this.type = type; this.type = type;
this.secret = secret;
if (projects && projects.length > 0) { if (projects && projects.length > 0) {
this.projects = projects; this.projects = projects;
} else { } else {

View File

@ -6,6 +6,7 @@ export const ALL = '*';
export enum ApiTokenType { export enum ApiTokenType {
CLIENT = 'client', CLIENT = 'client',
ADMIN = 'admin', ADMIN = 'admin',
PROXY = 'proxy',
} }
export interface ILegacyApiTokenCreate { export interface ILegacyApiTokenCreate {
@ -102,6 +103,12 @@ export const validateApiToken = ({
'Client token cannot be scoped to all environments', '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 = ( export const validateApiTokenEnvironment = (

View File

@ -1,6 +1,7 @@
//Special //Special
export const ADMIN = 'ADMIN'; export const ADMIN = 'ADMIN';
export const CLIENT = 'CLIENT'; export const CLIENT = 'CLIENT';
export const PROXY = 'PROXY';
export const NONE = 'NONE'; export const NONE = 'NONE';
export const CREATE_FEATURE = 'CREATE_FEATURE'; export const CREATE_FEATURE = 'CREATE_FEATURE';

View File

@ -1862,6 +1862,7 @@ test('Should not allow changing project to target project without the same enabl
project: '*', project: '*',
type: ApiTokenType.ADMIN, type: ApiTokenType.ADMIN,
environment: '*', environment: '*',
secret: 'a',
}); });
await expect(async () => await expect(async () =>
app.services.projectService.changeProject( app.services.projectService.changeProject(
@ -1945,6 +1946,7 @@ test('Should allow changing project to target project with the same enabled envi
project: '*', project: '*',
type: ApiTokenType.ADMIN, type: ApiTokenType.ADMIN,
environment: '*', environment: '*',
secret: 'a',
}); });
await expect(async () => await expect(async () =>
app.services.projectService.changeProject( app.services.projectService.changeProject(

View File

@ -230,7 +230,7 @@ Object {
"type": "string", "type": "string",
}, },
"type": Object { "type": Object {
"description": "client, admin.", "description": "client, admin, proxy.",
"type": "string", "type": "string",
}, },
"username": Object { "username": Object {
@ -667,7 +667,7 @@ Object {
"type": "string", "type": "string",
}, },
"type": Object { "type": Object {
"description": "client, admin.", "description": "client, admin, proxy.",
"type": "string", "type": "string",
}, },
"username": Object { "username": Object {