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:
parent
45c6464587
commit
9676165de9
@ -45,6 +45,7 @@ const tokenRowReducer = (acc, tokenRow) => {
|
|||||||
environment: token.environment ? token.environment : ALL,
|
environment: token.environment ? token.environment : ALL,
|
||||||
expiresAt: token.expires_at,
|
expiresAt: token.expires_at,
|
||||||
createdAt: token.created_at,
|
createdAt: token.created_at,
|
||||||
|
metadata: token.metadata,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const currentToken = acc[tokenRow.secret];
|
const currentToken = acc[tokenRow.secret];
|
||||||
@ -65,6 +66,7 @@ const toRow = (newToken: IApiTokenCreate) => ({
|
|||||||
environment:
|
environment:
|
||||||
newToken.environment === ALL ? undefined : newToken.environment,
|
newToken.environment === ALL ? undefined : newToken.environment,
|
||||||
expires_at: newToken.expiresAt,
|
expires_at: newToken.expiresAt,
|
||||||
|
metadata: newToken.metadata || {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const toTokens = (rows: any[]): IApiToken[] => {
|
const toTokens = (rows: any[]): IApiToken[] => {
|
||||||
@ -124,6 +126,7 @@ export class ApiTokenStore implements IApiTokenStore {
|
|||||||
'type',
|
'type',
|
||||||
'expires_at',
|
'expires_at',
|
||||||
'created_at',
|
'created_at',
|
||||||
|
'metadata',
|
||||||
'seen_at',
|
'seen_at',
|
||||||
'environment',
|
'environment',
|
||||||
'token_project_link.project',
|
'token_project_link.project',
|
||||||
@ -150,6 +153,7 @@ export class ApiTokenStore implements IApiTokenStore {
|
|||||||
await Promise.all(updateProjectTasks);
|
await Promise.all(updateProjectTasks);
|
||||||
return {
|
return {
|
||||||
...newToken,
|
...newToken,
|
||||||
|
metadata: newToken.metadata || {},
|
||||||
project: newToken.projects?.join(',') || '*',
|
project: newToken.projects?.join(',') || '*',
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
};
|
};
|
||||||
|
@ -16,3 +16,20 @@ Object {
|
|||||||
"schema": "#/components/schemas/apiTokenSchema",
|
"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",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -2,24 +2,64 @@ import { ApiTokenType } from '../../types/models/api-token';
|
|||||||
import { validateSchema } from '../validate';
|
import { validateSchema } from '../validate';
|
||||||
import { ApiTokenSchema } from './api-token-schema';
|
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', () => {
|
test('apiTokenSchema', () => {
|
||||||
const data: ApiTokenSchema = {
|
const data: ApiTokenSchema = { ...defaultData };
|
||||||
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: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
validateSchema('#/components/schemas/apiTokenSchema', data),
|
validateSchema('#/components/schemas/apiTokenSchema', data),
|
||||||
).toBeUndefined();
|
).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', () => {
|
test('apiTokenSchema empty', () => {
|
||||||
expect(
|
expect(
|
||||||
validateSchema('#/components/schemas/apiTokenSchema', {}),
|
validateSchema('#/components/schemas/apiTokenSchema', {}),
|
||||||
|
@ -44,6 +44,22 @@ export const apiTokenSchema = {
|
|||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
},
|
},
|
||||||
|
metadata: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
properties: {
|
||||||
|
corsOrigins: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
alias: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {},
|
components: {},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -42,3 +42,40 @@ test('should not have projects set if project is present', async () => {
|
|||||||
});
|
});
|
||||||
expect(token.projects).not.toBeDefined();
|
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');
|
||||||
|
});
|
||||||
|
@ -10,7 +10,7 @@ export const createApiToken = joi
|
|||||||
.string()
|
.string()
|
||||||
.lowercase()
|
.lowercase()
|
||||||
.required()
|
.required()
|
||||||
.valid(ApiTokenType.ADMIN, ApiTokenType.CLIENT),
|
.valid(ApiTokenType.ADMIN, ApiTokenType.CLIENT, ApiTokenType.PROXY),
|
||||||
expiresAt: joi.date().optional(),
|
expiresAt: joi.date().optional(),
|
||||||
project: joi.when('projects', {
|
project: joi.when('projects', {
|
||||||
not: joi.required(),
|
not: joi.required(),
|
||||||
@ -18,7 +18,7 @@ export const createApiToken = joi
|
|||||||
}),
|
}),
|
||||||
projects: joi.array().min(0).optional(),
|
projects: joi.array().min(0).optional(),
|
||||||
environment: joi.when('type', {
|
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),
|
then: joi.string().optional().default(DEFAULT_ENV),
|
||||||
otherwise: joi.string().optional().default(ALL),
|
otherwise: joi.string().optional().default(ALL),
|
||||||
}),
|
}),
|
||||||
|
@ -102,7 +102,14 @@ export class ApiTokenService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getUserForToken(secret: string): ApiUser | undefined {
|
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) {
|
if (token) {
|
||||||
return new ApiUser({
|
return new ApiUser({
|
||||||
username: token.username,
|
username: token.username,
|
||||||
|
@ -22,17 +22,21 @@ export interface ILegacyApiTokenCreate {
|
|||||||
export interface IApiTokenCreate {
|
export interface IApiTokenCreate {
|
||||||
secret: string;
|
secret: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
metadata?: Metadata;
|
||||||
type: ApiTokenType;
|
type: ApiTokenType;
|
||||||
environment: string;
|
environment: string;
|
||||||
projects: string[];
|
projects: string[];
|
||||||
expiresAt?: Date;
|
expiresAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Metadata = { [key: string]: unknown };
|
||||||
|
|
||||||
export interface IApiToken extends IApiTokenCreate {
|
export interface IApiToken extends IApiTokenCreate {
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
seenAt?: Date;
|
seenAt?: Date;
|
||||||
environment: string;
|
environment: string;
|
||||||
project: string;
|
project: string;
|
||||||
|
metadata: Metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isAllProjects = (projects: string[]): boolean => {
|
export const isAllProjects = (projects: string[]): boolean => {
|
||||||
|
@ -212,6 +212,22 @@ Object {
|
|||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string",
|
"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 {
|
"project": Object {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
1
src/test/fixtures/fake-api-token-store.ts
vendored
1
src/test/fixtures/fake-api-token-store.ts
vendored
@ -52,6 +52,7 @@ export default class FakeApiTokenStore
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
project: newToken.projects?.join(',') || '*',
|
project: newToken.projects?.join(',') || '*',
|
||||||
...newToken,
|
...newToken,
|
||||||
|
metadata: {},
|
||||||
};
|
};
|
||||||
this.tokens.push(apiToken);
|
this.tokens.push(apiToken);
|
||||||
this.emit('insert');
|
this.emit('insert');
|
||||||
|
Loading…
Reference in New Issue
Block a user