1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

Refactor: rename frontend api key (#1935)

* refactor: rename frontend api key

* fix: api token schema tests
This commit is contained in:
Tymoteusz Czech 2022-08-18 10:20:51 +02:00 committed by GitHub
parent 037b8eacd3
commit 3266e9c22a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 125 additions and 73 deletions

View File

@ -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;

View File

@ -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: ['*'],

View File

@ -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),
}),

View File

@ -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);
});

View File

@ -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<void> {
async fetchActiveTokens(): Promise<void> {
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) {

View File

@ -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);
}
}

View File

@ -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',
);
}
};

View File

@ -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';

View File

@ -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 {

View File

@ -88,7 +88,7 @@ const createProject = async (id: string): Promise<void> => {
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) => {

View File

@ -44,7 +44,9 @@ export default class FakeApiTokenStore
}
async getAllActive(): Promise<IApiToken[]> {
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<IApiToken> {