1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

Feat: Create frontend API key (#1932)

* refactor: remove unused API definition routes

* feat: embed proxy endpoints

* feat: add metadata

* feat: update schema

* feat: check token metadata for alias if none is found

* refactor: add api token metadata test coverage

* refactor: proxy key validation and default values

* refactor: update snapshot

Co-authored-by: olav <mail@olav.io>
Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
Tymoteusz Czech 2022-08-17 10:55:52 +02:00 committed by GitHub
parent 45c6464587
commit 9676165de9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 156 additions and 14 deletions

View File

@ -45,6 +45,7 @@ const tokenRowReducer = (acc, tokenRow) => {
environment: token.environment ? token.environment : ALL,
expiresAt: token.expires_at,
createdAt: token.created_at,
metadata: token.metadata,
};
}
const currentToken = acc[tokenRow.secret];
@ -65,6 +66,7 @@ const toRow = (newToken: IApiTokenCreate) => ({
environment:
newToken.environment === ALL ? undefined : newToken.environment,
expires_at: newToken.expiresAt,
metadata: newToken.metadata || {},
});
const toTokens = (rows: any[]): IApiToken[] => {
@ -124,6 +126,7 @@ export class ApiTokenStore implements IApiTokenStore {
'type',
'expires_at',
'created_at',
'metadata',
'seen_at',
'environment',
'token_project_link.project',
@ -150,6 +153,7 @@ export class ApiTokenStore implements IApiTokenStore {
await Promise.all(updateProjectTasks);
return {
...newToken,
metadata: newToken.metadata || {},
project: newToken.projects?.join(',') || '*',
createdAt: row.created_at,
};

View File

@ -16,3 +16,20 @@ Object {
"schema": "#/components/schemas/apiTokenSchema",
}
`;
exports[`apiTokenSchema metadata - does not allow custom metadata parameters 1`] = `
Object {
"errors": Array [
Object {
"instancePath": "/metadata",
"keyword": "additionalProperties",
"message": "must NOT have additional properties",
"params": Object {
"additionalProperty": "arbitraryParameter",
},
"schemaPath": "#/properties/metadata/additionalProperties",
},
],
"schema": "#/components/schemas/apiTokenSchema",
}
`;

View File

@ -2,24 +2,64 @@ import { ApiTokenType } from '../../types/models/api-token';
import { validateSchema } from '../validate';
import { ApiTokenSchema } from './api-token-schema';
const defaultData: ApiTokenSchema = {
secret: '',
username: '',
type: ApiTokenType.CLIENT,
environment: '',
projects: [],
expiresAt: '2022-01-01T00:00:00.000Z',
createdAt: '2022-01-01T00:00:00.000Z',
seenAt: '2022-01-01T00:00:00.000Z',
project: '',
};
test('apiTokenSchema', () => {
const data: ApiTokenSchema = {
secret: '',
username: '',
type: ApiTokenType.CLIENT,
environment: '',
projects: [],
expiresAt: '2022-01-01T00:00:00.000Z',
createdAt: '2022-01-01T00:00:00.000Z',
seenAt: '2022-01-01T00:00:00.000Z',
project: '',
};
const data: ApiTokenSchema = { ...defaultData };
expect(
validateSchema('#/components/schemas/apiTokenSchema', data),
).toBeUndefined();
});
test('apiTokenSchema metadata - should allow empty object', () => {
const data: ApiTokenSchema = { ...defaultData, metadata: {} };
expect(
validateSchema('#/components/schemas/apiTokenSchema', data),
).toBeUndefined();
});
test('apiTokenSchema metadata - allows for corsOrigins and/or alias', () => {
expect(
validateSchema('#/components/schemas/apiTokenSchema', {
...defaultData,
metadata: { corsOrigins: ['*'] },
}),
).toBeUndefined();
expect(
validateSchema('#/components/schemas/apiTokenSchema', {
...defaultData,
metadata: { alias: 'secret' },
}),
).toBeUndefined();
expect(
validateSchema('#/components/schemas/apiTokenSchema', {
...defaultData,
metadata: { corsOrigins: ['*'], alias: 'abc' },
}),
).toBeUndefined();
});
test('apiTokenSchema metadata - does not allow custom metadata parameters', () => {
expect(
validateSchema('#/components/schemas/apiTokenSchema', {
...defaultData,
metadata: { arbitraryParameter: true },
}),
).toMatchSnapshot();
});
test('apiTokenSchema empty', () => {
expect(
validateSchema('#/components/schemas/apiTokenSchema', {}),

View File

@ -44,6 +44,22 @@ export const apiTokenSchema = {
format: 'date-time',
nullable: true,
},
metadata: {
type: 'object',
additionalProperties: false,
properties: {
corsOrigins: {
type: 'array',
items: {
type: 'string',
},
},
alias: {
type: 'string',
},
},
nullable: true,
},
},
components: {},
} as const;

View File

@ -42,3 +42,40 @@ test('should not have projects set if project is present', async () => {
});
expect(token.projects).not.toBeDefined();
});
test('should set metadata', async () => {
let token = await createApiToken.validateAsync({
username: 'test',
type: 'admin',
project: 'default',
metadata: {
corsOrigins: ['*'],
alias: 'secret',
},
});
expect(token.projects).toBeUndefined();
});
test('should allow for embedded proxy (frontend) key', async () => {
let token = await createApiToken.validateAsync({
username: 'test',
type: 'proxy',
project: 'default',
metadata: {
corsOrigins: ['*'],
},
});
expect(token.error).toBeUndefined();
});
test('should set environment to default for proxy key', async () => {
let token = await createApiToken.validateAsync({
username: 'test',
type: 'proxy',
project: 'default',
metadata: {
corsOrigins: ['*'],
},
});
expect(token.environment).toEqual('default');
});

View File

@ -10,7 +10,7 @@ export const createApiToken = joi
.string()
.lowercase()
.required()
.valid(ApiTokenType.ADMIN, ApiTokenType.CLIENT),
.valid(ApiTokenType.ADMIN, ApiTokenType.CLIENT, ApiTokenType.PROXY),
expiresAt: joi.date().optional(),
project: joi.when('projects', {
not: joi.required(),
@ -18,7 +18,7 @@ export const createApiToken = joi
}),
projects: joi.array().min(0).optional(),
environment: joi.when('type', {
is: joi.string().valid(ApiTokenType.CLIENT),
is: joi.string().valid(ApiTokenType.CLIENT, ApiTokenType.PROXY),
then: joi.string().optional().default(DEFAULT_ENV),
otherwise: joi.string().optional().default(ALL),
}),

View File

@ -102,7 +102,14 @@ export class ApiTokenService {
}
public getUserForToken(secret: string): ApiUser | undefined {
const token = this.activeTokens.find((t) => t.secret === secret);
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) {
return new ApiUser({
username: token.username,

View File

@ -22,17 +22,21 @@ export interface ILegacyApiTokenCreate {
export interface IApiTokenCreate {
secret: string;
username: string;
metadata?: Metadata;
type: ApiTokenType;
environment: string;
projects: string[];
expiresAt?: Date;
}
type Metadata = { [key: string]: unknown };
export interface IApiToken extends IApiTokenCreate {
createdAt: Date;
seenAt?: Date;
environment: string;
project: string;
metadata: Metadata;
}
export const isAllProjects = (projects: string[]): boolean => {

View File

@ -212,6 +212,22 @@ Object {
"nullable": true,
"type": "string",
},
"metadata": Object {
"additionalProperties": false,
"nullable": true,
"properties": Object {
"alias": Object {
"type": "string",
},
"corsOrigins": Object {
"items": Object {
"type": "string",
},
"type": "array",
},
},
"type": "object",
},
"project": Object {
"type": "string",
},

View File

@ -52,6 +52,7 @@ export default class FakeApiTokenStore
createdAt: new Date(),
project: newToken.projects?.join(',') || '*',
...newToken,
metadata: {},
};
this.tokens.push(apiToken);
this.emit('insert');