mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-07 01:16:28 +02:00
chore(apitoken)!: remove ILegacyApiTokenCreate (#10072)
This commit is contained in:
parent
37548c3436
commit
1aadbb3641
@ -27,7 +27,7 @@ test('should create default config', async () => {
|
|||||||
test('should add initApiToken for admin token from options', async () => {
|
test('should add initApiToken for admin token from options', async () => {
|
||||||
const token = {
|
const token = {
|
||||||
environment: '*',
|
environment: '*',
|
||||||
project: '*',
|
projects: ['*'],
|
||||||
secret: '*:*.some-random-string',
|
secret: '*:*.some-random-string',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
tokenName: 'admin',
|
tokenName: 'admin',
|
||||||
@ -52,7 +52,9 @@ test('should add initApiToken for admin token from options', async () => {
|
|||||||
expect(config.authentication.initApiTokens[0].environment).toBe(
|
expect(config.authentication.initApiTokens[0].environment).toBe(
|
||||||
token.environment,
|
token.environment,
|
||||||
);
|
);
|
||||||
expect(config.authentication.initApiTokens[0].project).toBe(token.project);
|
expect(config.authentication.initApiTokens[0].projects).toMatchObject(
|
||||||
|
token.projects,
|
||||||
|
);
|
||||||
expect(config.authentication.initApiTokens[0].type).toBe(
|
expect(config.authentication.initApiTokens[0].type).toBe(
|
||||||
ApiTokenType.ADMIN,
|
ApiTokenType.ADMIN,
|
||||||
);
|
);
|
||||||
@ -61,7 +63,7 @@ test('should add initApiToken for admin token from options', async () => {
|
|||||||
test('should add initApiToken for client token from options', async () => {
|
test('should add initApiToken for client token from options', async () => {
|
||||||
const token = {
|
const token = {
|
||||||
environment: 'development',
|
environment: 'development',
|
||||||
project: 'default',
|
projects: ['default'],
|
||||||
secret: 'default:development.some-random-string',
|
secret: 'default:development.some-random-string',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
tokenName: 'admin',
|
tokenName: 'admin',
|
||||||
@ -86,7 +88,9 @@ test('should add initApiToken for client token from options', async () => {
|
|||||||
expect(config.authentication.initApiTokens[0].environment).toBe(
|
expect(config.authentication.initApiTokens[0].environment).toBe(
|
||||||
token.environment,
|
token.environment,
|
||||||
);
|
);
|
||||||
expect(config.authentication.initApiTokens[0].project).toBe(token.project);
|
expect(config.authentication.initApiTokens[0].projects).toMatchObject(
|
||||||
|
token.projects,
|
||||||
|
);
|
||||||
expect(config.authentication.initApiTokens[0].type).toBe(
|
expect(config.authentication.initApiTokens[0].type).toBe(
|
||||||
ApiTokenType.CLIENT,
|
ApiTokenType.CLIENT,
|
||||||
);
|
);
|
||||||
@ -110,7 +114,9 @@ test('should add initApiToken for admin token from env var', async () => {
|
|||||||
|
|
||||||
expect(config.authentication.initApiTokens).toHaveLength(2);
|
expect(config.authentication.initApiTokens).toHaveLength(2);
|
||||||
expect(config.authentication.initApiTokens[0].environment).toBe('*');
|
expect(config.authentication.initApiTokens[0].environment).toBe('*');
|
||||||
expect(config.authentication.initApiTokens[0].project).toBe('*');
|
expect(config.authentication.initApiTokens[0].projects).toMatchObject([
|
||||||
|
'*',
|
||||||
|
]);
|
||||||
expect(config.authentication.initApiTokens[0].type).toBe(
|
expect(config.authentication.initApiTokens[0].type).toBe(
|
||||||
ApiTokenType.ADMIN,
|
ApiTokenType.ADMIN,
|
||||||
);
|
);
|
||||||
@ -146,7 +152,7 @@ test('should merge initApiToken from options and env vars', async () => {
|
|||||||
process.env.INIT_CLIENT_API_TOKENS = 'default:development.some-token1';
|
process.env.INIT_CLIENT_API_TOKENS = 'default:development.some-token1';
|
||||||
const token = {
|
const token = {
|
||||||
environment: '*',
|
environment: '*',
|
||||||
project: '*',
|
projects: ['*'],
|
||||||
secret: '*:*.some-random-string',
|
secret: '*:*.some-random-string',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
tokenName: 'admin',
|
tokenName: 'admin',
|
||||||
@ -193,7 +199,9 @@ test('should add initApiToken for client token from env var', async () => {
|
|||||||
expect(config.authentication.initApiTokens[0].environment).toBe(
|
expect(config.authentication.initApiTokens[0].environment).toBe(
|
||||||
'development',
|
'development',
|
||||||
);
|
);
|
||||||
expect(config.authentication.initApiTokens[0].project).toBe('default');
|
expect(config.authentication.initApiTokens[0].projects).toMatchObject([
|
||||||
|
'default',
|
||||||
|
]);
|
||||||
expect(config.authentication.initApiTokens[0].type).toBe(
|
expect(config.authentication.initApiTokens[0].type).toBe(
|
||||||
ApiTokenType.CLIENT,
|
ApiTokenType.CLIENT,
|
||||||
);
|
);
|
||||||
@ -207,7 +215,7 @@ test('should add initApiToken for client token from env var', async () => {
|
|||||||
test('should handle cases where no env var specified for tokens', async () => {
|
test('should handle cases where no env var specified for tokens', async () => {
|
||||||
const token = {
|
const token = {
|
||||||
environment: '*',
|
environment: '*',
|
||||||
project: '*',
|
projects: ['*'],
|
||||||
secret: '*:*.some-random-string',
|
secret: '*:*.some-random-string',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
tokenName: 'admin',
|
tokenName: 'admin',
|
||||||
@ -506,7 +514,7 @@ test('create config should be idempotent in terms of tokens', async () => {
|
|||||||
process.env.INIT_FRONTEND_API_TOKENS = 'frontend:development.some-token1';
|
process.env.INIT_FRONTEND_API_TOKENS = 'frontend:development.some-token1';
|
||||||
const token = {
|
const token = {
|
||||||
environment: '*',
|
environment: '*',
|
||||||
project: '*',
|
projects: ['*'],
|
||||||
secret: '*:*.some-random-string',
|
secret: '*:*.some-random-string',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
tokenName: 'admin',
|
tokenName: 'admin',
|
||||||
|
@ -37,7 +37,7 @@ import {
|
|||||||
secondsToMilliseconds,
|
secondsToMilliseconds,
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import { mapLegacyToken, validateApiToken } from './types/models/api-token.js';
|
import { validateApiToken } from './types/models/api-token.js';
|
||||||
import {
|
import {
|
||||||
parseEnvVarBoolean,
|
parseEnvVarBoolean,
|
||||||
parseEnvVarJSON,
|
parseEnvVarJSON,
|
||||||
@ -417,13 +417,13 @@ const loadTokensFromString = (
|
|||||||
const [environment = '*'] = rest.split('.');
|
const [environment = '*'] = rest.split('.');
|
||||||
const token = {
|
const token = {
|
||||||
createdAt: undefined,
|
createdAt: undefined,
|
||||||
project,
|
projects: [project],
|
||||||
environment,
|
environment,
|
||||||
secret,
|
secret,
|
||||||
type: tokenType,
|
type: tokenType,
|
||||||
tokenName: 'admin',
|
tokenName: 'admin',
|
||||||
};
|
};
|
||||||
validateApiToken(mapLegacyToken(token));
|
validateApiToken(token);
|
||||||
return token;
|
return token;
|
||||||
});
|
});
|
||||||
return tokens;
|
return tokens;
|
||||||
|
@ -50,7 +50,6 @@ const tokenRowReducer = (acc, tokenRow) => {
|
|||||||
createdAt: token.created_at,
|
createdAt: token.created_at,
|
||||||
alias: token.alias,
|
alias: token.alias,
|
||||||
seenAt: token.seen_at,
|
seenAt: token.seen_at,
|
||||||
username: token.token_name ? token.token_name : token.username,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const currentToken = acc[tokenRow.secret];
|
const currentToken = acc[tokenRow.secret];
|
||||||
@ -65,8 +64,8 @@ const tokenRowReducer = (acc, tokenRow) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toRow = (newToken: IApiTokenCreate) => ({
|
const toRow = (newToken: IApiTokenCreate) => ({
|
||||||
username: newToken.tokenName ?? newToken.username,
|
username: newToken.tokenName,
|
||||||
token_name: newToken.tokenName ?? newToken.username,
|
token_name: newToken.tokenName,
|
||||||
secret: newToken.secret,
|
secret: newToken.secret,
|
||||||
type: newToken.type,
|
type: newToken.type,
|
||||||
environment:
|
environment:
|
||||||
@ -192,7 +191,6 @@ export class ApiTokenStore implements IApiTokenStore {
|
|||||||
await Promise.all(updateProjectTasks);
|
await Promise.all(updateProjectTasks);
|
||||||
return {
|
return {
|
||||||
...newToken,
|
...newToken,
|
||||||
username: newToken.tokenName,
|
|
||||||
alias: newToken.alias || null,
|
alias: newToken.alias || null,
|
||||||
project: newToken.projects?.join(',') || '*',
|
project: newToken.projects?.join(',') || '*',
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
|
@ -2436,11 +2436,11 @@ test('should also delete api tokens that were only bound to deleted project', as
|
|||||||
auditUser,
|
auditUser,
|
||||||
);
|
);
|
||||||
|
|
||||||
const token = await apiTokenService.createApiToken({
|
const token = await apiTokenService.createApiTokenWithProjects({
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
tokenName,
|
tokenName,
|
||||||
environment: DEFAULT_ENV,
|
environment: DEFAULT_ENV,
|
||||||
project: project,
|
projects: [project],
|
||||||
});
|
});
|
||||||
|
|
||||||
await projectService.deleteProject(project, user, auditUser);
|
await projectService.deleteProject(project, user, auditUser);
|
||||||
@ -2471,7 +2471,7 @@ test('should not delete project-bound api tokens still bound to project', async
|
|||||||
auditUser,
|
auditUser,
|
||||||
);
|
);
|
||||||
|
|
||||||
const token = await apiTokenService.createApiToken({
|
const token = await apiTokenService.createApiTokenWithProjects({
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
tokenName,
|
tokenName,
|
||||||
environment: DEFAULT_ENV,
|
environment: DEFAULT_ENV,
|
||||||
@ -2507,7 +2507,7 @@ test('should delete project-bound api tokens when all projects they belong to ar
|
|||||||
auditUser,
|
auditUser,
|
||||||
);
|
);
|
||||||
|
|
||||||
const token = await apiTokenService.createApiToken({
|
const token = await apiTokenService.createApiTokenWithProjects({
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
tokenName,
|
tokenName,
|
||||||
environment: DEFAULT_ENV,
|
environment: DEFAULT_ENV,
|
||||||
|
@ -4,7 +4,6 @@ import type { ApiTokenSchema } from './api-token-schema.js';
|
|||||||
|
|
||||||
const defaultData: ApiTokenSchema = {
|
const defaultData: ApiTokenSchema = {
|
||||||
secret: '',
|
secret: '',
|
||||||
username: '',
|
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
environment: '',
|
environment: '',
|
||||||
|
@ -5,14 +5,7 @@ export const apiTokenSchema = {
|
|||||||
$id: '#/components/schemas/apiTokenSchema',
|
$id: '#/components/schemas/apiTokenSchema',
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
required: [
|
required: ['secret', 'tokenName', 'type', 'projects', 'createdAt'],
|
||||||
'secret',
|
|
||||||
'tokenName',
|
|
||||||
'type',
|
|
||||||
'project',
|
|
||||||
'projects',
|
|
||||||
'createdAt',
|
|
||||||
],
|
|
||||||
description:
|
description:
|
||||||
'An overview of an [Unleash API token](https://docs.getunleash.io/reference/api-tokens-and-client-keys).',
|
'An overview of an [Unleash API token](https://docs.getunleash.io/reference/api-tokens-and-client-keys).',
|
||||||
properties: {
|
properties: {
|
||||||
@ -21,13 +14,6 @@ export const apiTokenSchema = {
|
|||||||
description: 'The token used for authentication.',
|
description: 'The token used for authentication.',
|
||||||
example: 'project:environment.xyzrandomstring',
|
example: 'project:environment.xyzrandomstring',
|
||||||
},
|
},
|
||||||
username: {
|
|
||||||
type: 'string',
|
|
||||||
deprecated: true,
|
|
||||||
description:
|
|
||||||
'This property was deprecated in Unleash v5. Prefer the `tokenName` property instead.',
|
|
||||||
example: 'a-name',
|
|
||||||
},
|
|
||||||
tokenName: {
|
tokenName: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'A unique name for this particular token',
|
description: 'A unique name for this particular token',
|
||||||
|
@ -1,17 +1,5 @@
|
|||||||
import type { FromSchema } from 'json-schema-to-ts';
|
import type { FromSchema } from 'json-schema-to-ts';
|
||||||
import { mergeAllOfs } from '../util/all-of.js';
|
import { mergeAllOfs } from '../util/all-of.js';
|
||||||
const adminSchema = {
|
|
||||||
required: ['type'],
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
type: {
|
|
||||||
type: 'string',
|
|
||||||
pattern: '^[Aa][Dd][Mm][Ii][Nn]$',
|
|
||||||
description: `An admin token. Must be the string "admin" (not case sensitive).`,
|
|
||||||
example: 'admin',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const tokenNameSchema = {
|
const tokenNameSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@ -25,20 +13,6 @@ const tokenNameSchema = {
|
|||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const usernameSchema = {
|
|
||||||
type: 'object',
|
|
||||||
required: ['username'],
|
|
||||||
properties: {
|
|
||||||
username: {
|
|
||||||
deprecated: true,
|
|
||||||
type: 'string',
|
|
||||||
description:
|
|
||||||
'The name of the token. This property was deprecated in v5. Use `tokenName` instead.',
|
|
||||||
example: 'token-64523',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const clientFrontendSchema = {
|
const clientFrontendSchema = {
|
||||||
required: ['type'],
|
required: ['type'],
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@ -100,12 +74,7 @@ export const createApiTokenSchema = {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
description:
|
description:
|
||||||
'The data required to create an [Unleash API token](https://docs.getunleash.io/reference/api-tokens-and-client-keys).',
|
'The data required to create an [Unleash API token](https://docs.getunleash.io/reference/api-tokens-and-client-keys).',
|
||||||
oneOf: [
|
oneOf: [mergeAllOfs([expireSchema, clientFrontendSchema, tokenNameSchema])],
|
||||||
mergeAllOfs([expireSchema, adminSchema, tokenNameSchema]),
|
|
||||||
mergeAllOfs([expireSchema, adminSchema, usernameSchema]),
|
|
||||||
mergeAllOfs([expireSchema, clientFrontendSchema, tokenNameSchema]),
|
|
||||||
mergeAllOfs([expireSchema, clientFrontendSchema, usernameSchema]),
|
|
||||||
],
|
|
||||||
components: {},
|
components: {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
41
src/lib/openapi/spec/create-project-api-token-schema.ts
Normal file
41
src/lib/openapi/spec/create-project-api-token-schema.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import type { FromSchema } from 'json-schema-to-ts';
|
||||||
|
|
||||||
|
export const createProjectApiTokenSchema = {
|
||||||
|
type: 'object',
|
||||||
|
required: ['tokenName', 'type'],
|
||||||
|
$id: '#/components/schemas/createProjectApiTokenSchema',
|
||||||
|
description:
|
||||||
|
'The schema for creating a project API token. This schema is used to create a new project API token.',
|
||||||
|
properties: {
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
pattern:
|
||||||
|
'^([Cc][Ll][Ii][Ee][Nn][Tt]|[Ff][Rr][Oo][Nn][Tt][Ee][Nn][Dd])$',
|
||||||
|
description: `A client or frontend token. Must be one of the strings "client" or "frontend" (not case sensitive).`,
|
||||||
|
example: 'frontend',
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'The environment that the token should be valid for. Defaults to "default".',
|
||||||
|
example: 'development',
|
||||||
|
default: 'default',
|
||||||
|
},
|
||||||
|
expiresAt: {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'The date and time when the token should expire. The date should be in ISO 8601 format.',
|
||||||
|
example: '2023-10-01T00:00:00Z',
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
|
tokenName: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'A unique name for this particular token',
|
||||||
|
example: 'some-user',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {},
|
||||||
|
} as const;
|
||||||
|
export type CreateProjectApiTokenSchema = FromSchema<
|
||||||
|
typeof createProjectApiTokenSchema
|
||||||
|
>;
|
@ -53,6 +53,7 @@ export * from './create-feature-strategy-schema.js';
|
|||||||
export * from './create-group-schema.js';
|
export * from './create-group-schema.js';
|
||||||
export * from './create-invited-user-schema.js';
|
export * from './create-invited-user-schema.js';
|
||||||
export * from './create-pat-schema.js';
|
export * from './create-pat-schema.js';
|
||||||
|
export * from './create-project-api-token-schema.js';
|
||||||
export * from './create-strategy-schema.js';
|
export * from './create-strategy-schema.js';
|
||||||
export * from './create-strategy-variant-schema.js';
|
export * from './create-strategy-variant-schema.js';
|
||||||
export * from './create-tag-schema.js';
|
export * from './create-tag-schema.js';
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
import permissions from '../../../test/fixtures/permissions.js';
|
|
||||||
import { createTestConfig } from '../../../test/config/test-config.js';
|
|
||||||
import createStores from '../../../test/fixtures/store.js';
|
|
||||||
import { createServices } from '../../services/index.js';
|
|
||||||
import getApp from '../../app.js';
|
|
||||||
import supertest from 'supertest';
|
|
||||||
import { addDays } from 'date-fns';
|
|
||||||
|
|
||||||
async function getSetup() {
|
|
||||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
|
||||||
const perms = permissions();
|
|
||||||
const config = createTestConfig({
|
|
||||||
preHook: perms.hook,
|
|
||||||
server: { baseUriPath: base },
|
|
||||||
//@ts-ignore - Just testing, so only need the isEnabled call here
|
|
||||||
});
|
|
||||||
const stores = createStores();
|
|
||||||
const services = createServices(stores, config);
|
|
||||||
|
|
||||||
//@ts-expect-error: we're accessing a private field, but we need
|
|
||||||
//to set up an environment to test the functionality. Because we
|
|
||||||
//don't have a db to use, we need to access the service's store
|
|
||||||
//directly.
|
|
||||||
await services.apiTokenService.environmentStore.create({
|
|
||||||
name: 'development',
|
|
||||||
type: 'development',
|
|
||||||
enabled: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const app = await getApp(config, stores, services);
|
|
||||||
|
|
||||||
return {
|
|
||||||
base,
|
|
||||||
request: supertest(app),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Admin token killswitch', () => {
|
|
||||||
test('If killswitch is on we will get an operation denied if we try to create an admin token', async () => {
|
|
||||||
const setup = await getSetup();
|
|
||||||
return setup.request
|
|
||||||
.post(`${setup.base}/api/admin/api-tokens`)
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.send({
|
|
||||||
expiresAt: addDays(new Date(), 60),
|
|
||||||
type: 'ADMIN',
|
|
||||||
tokenName: 'Killswitched',
|
|
||||||
})
|
|
||||||
.expect(403)
|
|
||||||
.expect((res) => {
|
|
||||||
expect(res.body.message).toBe(
|
|
||||||
'Admin tokens are disabled in this instance. Use a Service account or a PAT to access admin operations instead',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('If killswitch is on we can still create a client token', async () => {
|
|
||||||
const setup = await getSetup();
|
|
||||||
return setup.request
|
|
||||||
.post(`${setup.base}/api/admin/api-tokens`)
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.send({
|
|
||||||
expiresAt: addDays(new Date(), 60),
|
|
||||||
type: 'CLIENT',
|
|
||||||
environment: 'development',
|
|
||||||
projects: ['*'],
|
|
||||||
tokenName: 'Client',
|
|
||||||
})
|
|
||||||
.expect(201)
|
|
||||||
.expect((res) => {
|
|
||||||
expect(res.body.secret).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('If killswitch is on we can still create a frontend token', async () => {
|
|
||||||
const setup = await getSetup();
|
|
||||||
return setup.request
|
|
||||||
.post(`${setup.base}/api/admin/api-tokens`)
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.send({
|
|
||||||
expiresAt: addDays(new Date(), 60),
|
|
||||||
type: 'FRONTEND',
|
|
||||||
environment: 'development',
|
|
||||||
projects: ['*'],
|
|
||||||
tokenName: 'Frontend',
|
|
||||||
})
|
|
||||||
.expect(201)
|
|
||||||
.expect((res) => {
|
|
||||||
expect(res.body.secret).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -43,6 +43,7 @@ import {
|
|||||||
} from '../../openapi/util/standard-responses.js';
|
} from '../../openapi/util/standard-responses.js';
|
||||||
import type { FrontendApiService } from '../../features/frontend-api/frontend-api-service.js';
|
import type { FrontendApiService } from '../../features/frontend-api/frontend-api-service.js';
|
||||||
import { OperationDeniedError } from '../../error/index.js';
|
import { OperationDeniedError } from '../../error/index.js';
|
||||||
|
import type { CreateApiTokenSchema } from '../../internals.js';
|
||||||
|
|
||||||
interface TokenParam {
|
interface TokenParam {
|
||||||
token: string;
|
token: string;
|
||||||
@ -299,24 +300,20 @@ export class ApiTokenController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createApiToken(
|
async createApiToken(
|
||||||
req: IAuthRequest,
|
req: IAuthRequest<CreateApiTokenSchema>,
|
||||||
res: Response<ApiTokenSchema>,
|
res: Response<ApiTokenSchema>,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const createToken = await createApiToken.validateAsync(req.body);
|
const createToken = await createApiToken.validateAsync(req.body);
|
||||||
const permissionRequired = tokenTypeToCreatePermission(
|
const permissionRequired = tokenTypeToCreatePermission(
|
||||||
createToken.type,
|
createToken.type,
|
||||||
);
|
);
|
||||||
if (createToken.type.toUpperCase() === 'ADMIN') {
|
|
||||||
throw new OperationDeniedError(
|
|
||||||
`Admin tokens are disabled in this instance. Use a Service account or a PAT to access admin operations instead`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const hasPermission = await this.accessService.hasPermission(
|
const hasPermission = await this.accessService.hasPermission(
|
||||||
req.user,
|
req.user,
|
||||||
permissionRequired,
|
permissionRequired,
|
||||||
);
|
);
|
||||||
if (hasPermission) {
|
if (hasPermission) {
|
||||||
const token = await this.apiTokenService.createApiToken(
|
const token = await this.apiTokenService.createApiTokenWithProjects(
|
||||||
createToken,
|
createToken,
|
||||||
req.audit,
|
req.audit,
|
||||||
);
|
);
|
||||||
|
@ -32,8 +32,9 @@ import Controller from '../../controller.js';
|
|||||||
import type { Logger } from '../../../logger.js';
|
import type { Logger } from '../../../logger.js';
|
||||||
import type { Response } from 'express';
|
import type { Response } from 'express';
|
||||||
import { timingSafeEqual } from 'crypto';
|
import { timingSafeEqual } from 'crypto';
|
||||||
import { createApiToken } from '../../../schema/api-token-schema.js';
|
|
||||||
import { OperationDeniedError } from '../../../error/index.js';
|
import { OperationDeniedError } from '../../../error/index.js';
|
||||||
|
import type { CreateProjectApiTokenSchema } from '../../../openapi/spec/create-project-api-token-schema.js';
|
||||||
|
import { createProjectApiToken } from '../../../schema/create-project-api-token-schema.js';
|
||||||
|
|
||||||
interface ProjectTokenParam {
|
interface ProjectTokenParam {
|
||||||
token: string;
|
token: string;
|
||||||
@ -109,7 +110,9 @@ export class ProjectApiTokenController extends Controller {
|
|||||||
openApiService.validPath({
|
openApiService.validPath({
|
||||||
tags: ['Projects'],
|
tags: ['Projects'],
|
||||||
operationId: 'createProjectApiToken',
|
operationId: 'createProjectApiToken',
|
||||||
requestBody: createRequestSchema('createApiTokenSchema'),
|
requestBody: createRequestSchema(
|
||||||
|
'createProjectApiTokenSchema',
|
||||||
|
),
|
||||||
summary: 'Create a project API token.',
|
summary: 'Create a project API token.',
|
||||||
description:
|
description:
|
||||||
'Endpoint that allows creation of [project API tokens](https://docs.getunleash.io/reference/api-tokens-and-client-keys#api-token-visibility) for the specified project.',
|
'Endpoint that allows creation of [project API tokens](https://docs.getunleash.io/reference/api-tokens-and-client-keys#api-token-visibility) for the specified project.',
|
||||||
@ -160,10 +163,10 @@ export class ProjectApiTokenController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createProjectApiToken(
|
async createProjectApiToken(
|
||||||
req: IAuthRequest,
|
req: IAuthRequest<{ projectId: string }, CreateProjectApiTokenSchema>,
|
||||||
res: Response<ApiTokenSchema>,
|
res: Response<ApiTokenSchema>,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const createToken = await createApiToken.validateAsync(req.body);
|
const createToken = await createProjectApiToken.validateAsync(req.body);
|
||||||
const { projectId } = req.params;
|
const { projectId } = req.params;
|
||||||
await this.projectService.getProject(projectId); // Validates that the project exists
|
await this.projectService.getProject(projectId); // Validates that the project exists
|
||||||
|
|
||||||
@ -178,30 +181,17 @@ export class ProjectApiTokenController extends Controller {
|
|||||||
`You don't have the necessary access [${permissionRequired}] to perform this operation]`,
|
`You don't have the necessary access [${permissionRequired}] to perform this operation]`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!createToken.project) {
|
const token = await this.apiTokenService.createApiTokenWithProjects(
|
||||||
createToken.project = projectId;
|
{ ...createToken, projects: [projectId] },
|
||||||
}
|
req.audit,
|
||||||
|
);
|
||||||
if (
|
this.openApiService.respondWithValidation(
|
||||||
createToken.projects.length === 1 &&
|
201,
|
||||||
createToken.projects[0] === projectId
|
res,
|
||||||
) {
|
apiTokenSchema.$id,
|
||||||
const token = await this.apiTokenService.createApiToken(
|
serializeDates(token),
|
||||||
createToken,
|
{ location: `api-tokens` },
|
||||||
req.audit,
|
);
|
||||||
);
|
|
||||||
this.openApiService.respondWithValidation(
|
|
||||||
201,
|
|
||||||
res,
|
|
||||||
apiTokenSchema.$id,
|
|
||||||
serializeDates(token),
|
|
||||||
{ location: `api-tokens` },
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
res.statusMessage =
|
|
||||||
'Project level tokens can only be created for one project';
|
|
||||||
res.status(400);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteProjectApiToken(
|
async deleteProjectApiToken(
|
||||||
|
@ -1,51 +1,50 @@
|
|||||||
import { ALL } from '../types/models/api-token.js';
|
import { ALL } from '../types/models/api-token.js';
|
||||||
import { createApiToken } from './api-token-schema.js';
|
import { createApiToken } from './api-token-schema.js';
|
||||||
|
|
||||||
test('should reject token with projects and project', async () => {
|
test('should ignore token extra project field', async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(0);
|
||||||
try {
|
try {
|
||||||
await createApiToken.validateAsync({
|
await createApiToken.validateAsync({
|
||||||
username: 'test',
|
tokenName: 'test',
|
||||||
type: 'admin',
|
type: 'client',
|
||||||
project: 'default',
|
project: 'default',
|
||||||
projects: ['default'],
|
projects: ['default'],
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.details[0].message).toEqual(
|
expect(error).toBeUndefined();
|
||||||
'"project" must not exist simultaneously with [projects]',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not have default project set if projects is present', async () => {
|
test('should not have default project set if projects is present', async () => {
|
||||||
const token = await createApiToken.validateAsync({
|
const token = await createApiToken.validateAsync({
|
||||||
username: 'test',
|
tokenName: 'test',
|
||||||
type: 'admin',
|
type: 'client',
|
||||||
projects: ['default'],
|
projects: ['default'],
|
||||||
});
|
});
|
||||||
expect(token.project).not.toBeDefined();
|
expect(token.project).not.toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should have project set to default if projects is missing', async () => {
|
test('should have a projects entry consisting of ALL if projects is missing', async () => {
|
||||||
const token = await createApiToken.validateAsync({
|
const token = await createApiToken.validateAsync({
|
||||||
username: 'test',
|
tokenName: 'test',
|
||||||
type: 'admin',
|
type: 'client',
|
||||||
});
|
});
|
||||||
expect(token.project).toBe(ALL);
|
expect(token.projects).toMatchObject([ALL]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not have projects set if project is present', async () => {
|
test('should not have project set after validation if project is present', async () => {
|
||||||
const token = await createApiToken.validateAsync({
|
const token = await createApiToken.validateAsync({
|
||||||
username: 'test',
|
tokenName: 'test',
|
||||||
type: 'admin',
|
type: 'client',
|
||||||
project: 'default',
|
project: 'default',
|
||||||
});
|
});
|
||||||
expect(token.projects).not.toBeDefined();
|
expect(token.project).not.toBeDefined();
|
||||||
|
expect(token.projects).toMatchObject([ALL]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should allow for embedded proxy (frontend) key', async () => {
|
test('should allow for embedded proxy (frontend) key', async () => {
|
||||||
const token = await createApiToken.validateAsync({
|
const token = await createApiToken.validateAsync({
|
||||||
username: 'test',
|
tokenName: 'test',
|
||||||
type: 'frontend',
|
type: 'frontend',
|
||||||
project: 'default',
|
project: 'default',
|
||||||
});
|
});
|
||||||
@ -54,9 +53,8 @@ test('should allow for embedded proxy (frontend) key', async () => {
|
|||||||
|
|
||||||
test('should set environment to default for frontend key', async () => {
|
test('should set environment to default for frontend key', async () => {
|
||||||
const token = await createApiToken.validateAsync({
|
const token = await createApiToken.validateAsync({
|
||||||
username: 'test',
|
tokenName: 'test',
|
||||||
type: 'frontend',
|
type: 'frontend',
|
||||||
project: 'default',
|
|
||||||
});
|
});
|
||||||
expect(token.environment).toEqual('default');
|
expect(token.environment).toEqual('default');
|
||||||
});
|
});
|
||||||
|
@ -6,29 +6,18 @@ import { DEFAULT_ENV } from '../util/constants.js';
|
|||||||
export const createApiToken = joi
|
export const createApiToken = joi
|
||||||
.object()
|
.object()
|
||||||
.keys({
|
.keys({
|
||||||
username: joi.string().optional(),
|
tokenName: joi.string().required(),
|
||||||
tokenName: joi.string().optional(),
|
|
||||||
type: joi
|
type: joi
|
||||||
.string()
|
.string()
|
||||||
.lowercase()
|
.lowercase()
|
||||||
.required()
|
.required()
|
||||||
.valid(
|
.valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND),
|
||||||
ApiTokenType.ADMIN,
|
|
||||||
ApiTokenType.CLIENT,
|
|
||||||
ApiTokenType.FRONTEND,
|
|
||||||
),
|
|
||||||
expiresAt: joi.date().optional(),
|
expiresAt: joi.date().optional(),
|
||||||
project: joi.when('projects', {
|
projects: joi.array().min(1).optional().default([ALL]),
|
||||||
not: joi.required(),
|
|
||||||
then: joi.string().optional().default(ALL),
|
|
||||||
}),
|
|
||||||
projects: joi.array().min(0).optional(),
|
|
||||||
environment: joi.when('type', {
|
environment: joi.when('type', {
|
||||||
is: joi.string().valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND),
|
is: joi.string().valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND),
|
||||||
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),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.nand('username', 'tokenName')
|
|
||||||
.nand('project', 'projects')
|
|
||||||
.options({ stripUnknown: true, allowUnknown: false, abortEarly: false });
|
.options({ stripUnknown: true, allowUnknown: false, abortEarly: false });
|
||||||
|
20
src/lib/schema/create-project-api-token-schema.ts
Normal file
20
src/lib/schema/create-project-api-token-schema.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import joi from 'joi';
|
||||||
|
import { ApiTokenType } from '../types/model.js';
|
||||||
|
import { DEFAULT_ENV } from '../util/constants.js';
|
||||||
|
|
||||||
|
export const createProjectApiToken = joi
|
||||||
|
.object()
|
||||||
|
.keys({
|
||||||
|
tokenName: joi.string().required(),
|
||||||
|
type: joi
|
||||||
|
.string()
|
||||||
|
.lowercase()
|
||||||
|
.required()
|
||||||
|
.valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND),
|
||||||
|
expiresAt: joi.date().optional(),
|
||||||
|
environment: joi.when('type', {
|
||||||
|
is: joi.string().valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND),
|
||||||
|
then: joi.string().optional().default(DEFAULT_ENV),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.options({ stripUnknown: true, allowUnknown: false, abortEarly: false });
|
@ -19,7 +19,7 @@ import { vi } from 'vitest';
|
|||||||
test('Should init api token', async () => {
|
test('Should init api token', async () => {
|
||||||
const token = {
|
const token = {
|
||||||
environment: '*',
|
environment: '*',
|
||||||
project: '*',
|
projects: ['*'],
|
||||||
secret: '*:*:some-random-string',
|
secret: '*:*:some-random-string',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
tokenName: 'admin',
|
tokenName: 'admin',
|
||||||
|
@ -5,11 +5,9 @@ import type { IUnleashStores } from '../types/stores.js';
|
|||||||
import type { IUnleashConfig } from '../types/option.js';
|
import type { IUnleashConfig } from '../types/option.js';
|
||||||
import ApiUser, { type IApiUser } from '../types/api-user.js';
|
import ApiUser, { type IApiUser } from '../types/api-user.js';
|
||||||
import {
|
import {
|
||||||
type ILegacyApiTokenCreate,
|
resolveValidProjects,
|
||||||
validateApiToken,
|
validateApiToken,
|
||||||
validateApiTokenEnvironment,
|
validateApiTokenEnvironment,
|
||||||
mapLegacyToken,
|
|
||||||
mapLegacyTokenWithSecret,
|
|
||||||
} from '../types/models/api-token.js';
|
} from '../types/models/api-token.js';
|
||||||
import type { IApiTokenStore } from '../types/stores/api-token-store.js';
|
import type { IApiTokenStore } from '../types/stores/api-token-store.js';
|
||||||
import { FOREIGN_KEY_VIOLATION } from '../error/db-error.js';
|
import { FOREIGN_KEY_VIOLATION } from '../error/db-error.js';
|
||||||
@ -194,7 +192,7 @@ export class ApiTokenService {
|
|||||||
return this.store.getAll();
|
return this.store.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
async initApiTokens(tokens: ILegacyApiTokenCreate[]) {
|
async initApiTokens(tokens: IApiTokenCreate[]) {
|
||||||
const tokenCount = await this.store.count();
|
const tokenCount = await this.store.count();
|
||||||
if (tokenCount > 0) {
|
if (tokenCount > 0) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
@ -203,9 +201,9 @@ export class ApiTokenService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const createAll = tokens
|
const createAll = tokens.map((t) =>
|
||||||
.map(mapLegacyTokenWithSecret)
|
this.insertNewApiToken(t, SYSTEM_USER_AUDIT),
|
||||||
.map((t) => this.insertNewApiToken(t, SYSTEM_USER_AUDIT));
|
);
|
||||||
await Promise.all(createAll);
|
await Promise.all(createAll);
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`Created initial API tokens: ${tokens.map((t) => `(name: ${t.tokenName}, type: ${t.type})`).join(', ')}`,
|
`Created initial API tokens: ${tokens.map((t) => `(name: ${t.tokenName}, type: ${t.type})`).join(', ')}`,
|
||||||
@ -273,17 +271,6 @@ export class ApiTokenService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This may be removed in a future release, prefer createApiTokenWithProjects
|
|
||||||
*/
|
|
||||||
public async createApiToken(
|
|
||||||
newToken: Omit<ILegacyApiTokenCreate, 'secret'>,
|
|
||||||
auditUser: IAuditUser = SYSTEM_USER_AUDIT,
|
|
||||||
): Promise<IApiToken> {
|
|
||||||
const token = mapLegacyToken(newToken);
|
|
||||||
return this.internalCreateApiTokenWithProjects(token, auditUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param newToken
|
* @param newToken
|
||||||
* @param createdBy should be IApiUser or IUser. Still supports optional or string for backward compatibility
|
* @param createdBy should be IApiUser or IUser. Still supports optional or string for backward compatibility
|
||||||
@ -293,7 +280,13 @@ export class ApiTokenService {
|
|||||||
newToken: Omit<IApiTokenCreate, 'secret'>,
|
newToken: Omit<IApiTokenCreate, 'secret'>,
|
||||||
auditUser: IAuditUser = SYSTEM_USER_AUDIT,
|
auditUser: IAuditUser = SYSTEM_USER_AUDIT,
|
||||||
): Promise<IApiToken> {
|
): Promise<IApiToken> {
|
||||||
return this.internalCreateApiTokenWithProjects(newToken, auditUser);
|
return this.internalCreateApiTokenWithProjects(
|
||||||
|
{
|
||||||
|
...newToken,
|
||||||
|
projects: resolveValidProjects(newToken.projects),
|
||||||
|
},
|
||||||
|
auditUser,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async internalCreateApiTokenWithProjects(
|
private async internalCreateApiTokenWithProjects(
|
||||||
|
@ -210,10 +210,6 @@ export interface IApiTokenCreate {
|
|||||||
environment: string;
|
environment: string;
|
||||||
projects: string[];
|
projects: string[];
|
||||||
expiresAt?: Date;
|
expiresAt?: Date;
|
||||||
/**
|
|
||||||
* @deprecated Use tokenName instead
|
|
||||||
*/
|
|
||||||
username?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IApiToken extends Omit<IApiTokenCreate, 'alias'> {
|
export interface IApiToken extends Omit<IApiTokenCreate, 'alias'> {
|
||||||
|
@ -4,64 +4,16 @@ import { ApiTokenType } from '../model.js';
|
|||||||
|
|
||||||
export const ALL = '*';
|
export const ALL = '*';
|
||||||
|
|
||||||
export interface ILegacyApiTokenCreate {
|
|
||||||
secret: string;
|
|
||||||
/**
|
|
||||||
* @deprecated Use tokenName instead
|
|
||||||
*/
|
|
||||||
username?: string;
|
|
||||||
type: ApiTokenType;
|
|
||||||
environment?: string;
|
|
||||||
project?: string;
|
|
||||||
projects?: string[];
|
|
||||||
expiresAt?: Date;
|
|
||||||
tokenName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isAllProjects = (projects: string[]): boolean => {
|
export const isAllProjects = (projects: string[]): boolean => {
|
||||||
return projects && projects.length === 1 && projects[0] === ALL;
|
return projects && projects.length === 1 && projects[0] === ALL;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapLegacyProjects = (
|
export const resolveValidProjects = (projects: string[]): string[] => {
|
||||||
project?: string,
|
if (projects.includes('*')) {
|
||||||
projects?: string[],
|
return ['*'];
|
||||||
): string[] => {
|
|
||||||
let cleanedProjects: string[];
|
|
||||||
if (project) {
|
|
||||||
cleanedProjects = [project];
|
|
||||||
} else if (projects) {
|
|
||||||
cleanedProjects = projects;
|
|
||||||
if (cleanedProjects.includes('*')) {
|
|
||||||
cleanedProjects = ['*'];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new BadDataError(
|
|
||||||
'API tokens must either contain a project or projects field',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return cleanedProjects;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapLegacyToken = (
|
return projects;
|
||||||
token: Omit<ILegacyApiTokenCreate, 'secret'>,
|
|
||||||
): Omit<IApiTokenCreate, 'secret'> => {
|
|
||||||
const cleanedProjects = mapLegacyProjects(token.project, token.projects);
|
|
||||||
return {
|
|
||||||
tokenName: token.username ?? token.tokenName!,
|
|
||||||
type: token.type,
|
|
||||||
environment: token.environment || 'development',
|
|
||||||
projects: cleanedProjects,
|
|
||||||
expiresAt: token.expiresAt,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapLegacyTokenWithSecret = (
|
|
||||||
token: ILegacyApiTokenCreate,
|
|
||||||
): IApiTokenCreate => {
|
|
||||||
return {
|
|
||||||
...mapLegacyToken(token),
|
|
||||||
secret: token.secret,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateApiToken = ({
|
export const validateApiToken = ({
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { Express } from 'express';
|
import type { Express } from 'express';
|
||||||
import type EventEmitter from 'events';
|
import type EventEmitter from 'events';
|
||||||
import type { LogLevel, LogProvider } from '../logger.js';
|
import type { LogLevel, LogProvider } from '../logger.js';
|
||||||
import type { ILegacyApiTokenCreate } from './models/api-token.js';
|
import type { IApiTokenCreate } from './model.js';
|
||||||
import type {
|
import type {
|
||||||
IExperimentalOptions,
|
IExperimentalOptions,
|
||||||
IFlagContext,
|
IFlagContext,
|
||||||
@ -85,7 +85,7 @@ export interface IAuthOption {
|
|||||||
customAuthHandler?: CustomAuthHandler;
|
customAuthHandler?: CustomAuthHandler;
|
||||||
createAdminUser?: boolean;
|
createAdminUser?: boolean;
|
||||||
initialAdminUser?: UsernameAdminUser;
|
initialAdminUser?: UsernameAdminUser;
|
||||||
initApiTokens: ILegacyApiTokenCreate[];
|
initApiTokens: IApiTokenCreate[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IImportOption {
|
export interface IImportOption {
|
||||||
|
@ -63,7 +63,7 @@ process.nextTick(async () => {
|
|||||||
initApiTokens: [
|
initApiTokens: [
|
||||||
{
|
{
|
||||||
environment: '*',
|
environment: '*',
|
||||||
project: '*',
|
projects: ['*'],
|
||||||
secret: '*:*.964a287e1b728cb5f4f3e0120df92cb5',
|
secret: '*:*.964a287e1b728cb5f4f3e0120df92cb5',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
tokenName: 'some-user',
|
tokenName: 'some-user',
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
import {
|
import { setupAppWithCustomAuth } from '../../helpers/test-helper.js';
|
||||||
setupAppWithAuth,
|
|
||||||
setupAppWithCustomAuth,
|
|
||||||
} from '../../helpers/test-helper.js';
|
|
||||||
import dbInit, { type ITestDb } from '../../helpers/database-init.js';
|
import dbInit, { type ITestDb } from '../../helpers/database-init.js';
|
||||||
import getLogger from '../../../fixtures/no-logger.js';
|
import getLogger from '../../../fixtures/no-logger.js';
|
||||||
import { ApiTokenType } from '../../../../lib/types/model.js';
|
import { ApiTokenType } from '../../../../lib/types/model.js';
|
||||||
@ -16,7 +13,6 @@ import {
|
|||||||
SYSTEM_USER,
|
SYSTEM_USER,
|
||||||
SYSTEM_USER_AUDIT,
|
SYSTEM_USER_AUDIT,
|
||||||
SYSTEM_USER_ID,
|
SYSTEM_USER_ID,
|
||||||
TEST_AUDIT_USER,
|
|
||||||
UPDATE_CLIENT_API_TOKEN,
|
UPDATE_CLIENT_API_TOKEN,
|
||||||
} from '../../../../lib/types/index.js';
|
} from '../../../../lib/types/index.js';
|
||||||
import { addDays } from 'date-fns';
|
import { addDays } from 'date-fns';
|
||||||
@ -79,8 +75,7 @@ test('editor users should only get client or frontend tokens', async () => {
|
|||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'test',
|
||||||
username: 'test',
|
|
||||||
secret: '*:environment.1234',
|
secret: '*:environment.1234',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
});
|
});
|
||||||
@ -88,8 +83,7 @@ test('editor users should only get client or frontend tokens', async () => {
|
|||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'frontend',
|
||||||
username: 'frontend',
|
|
||||||
secret: '*:environment.12345',
|
secret: '*:environment.12345',
|
||||||
type: ApiTokenType.FRONTEND,
|
type: ApiTokenType.FRONTEND,
|
||||||
});
|
});
|
||||||
@ -97,8 +91,7 @@ test('editor users should only get client or frontend tokens', async () => {
|
|||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'test',
|
||||||
username: 'test',
|
|
||||||
secret: '*:*.sdfsdf2d',
|
secret: '*:*.sdfsdf2d',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
});
|
});
|
||||||
@ -141,8 +134,7 @@ test('viewer users should not be allowed to fetch tokens', async () => {
|
|||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'test',
|
||||||
username: 'test',
|
|
||||||
secret: '*:environment.1234',
|
secret: '*:environment.1234',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
});
|
});
|
||||||
@ -150,8 +142,7 @@ test('viewer users should not be allowed to fetch tokens', async () => {
|
|||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'test',
|
||||||
username: 'test',
|
|
||||||
secret: '*:*.sdfsdf2d',
|
secret: '*:*.sdfsdf2d',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
});
|
});
|
||||||
@ -164,102 +155,6 @@ test('viewer users should not be allowed to fetch tokens', async () => {
|
|||||||
await destroy();
|
await destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Only token-admins should be allowed to create token', async () => {
|
|
||||||
expect.assertions(0);
|
|
||||||
|
|
||||||
const preHook = (app, config, { userService, accessService }) => {
|
|
||||||
app.use('/api/admin/', async (req, res, next) => {
|
|
||||||
const role = await accessService.getPredefinedRole(RoleName.EDITOR);
|
|
||||||
req.user = await userService.createUser({
|
|
||||||
email: 'editor2@example.com',
|
|
||||||
rootRole: role.id,
|
|
||||||
});
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const { request, destroy } = await setupAppWithCustomAuth(
|
|
||||||
stores,
|
|
||||||
preHook,
|
|
||||||
undefined,
|
|
||||||
db.rawDatabase,
|
|
||||||
);
|
|
||||||
|
|
||||||
await request
|
|
||||||
.post('/api/admin/api-tokens')
|
|
||||||
.send({
|
|
||||||
username: 'default-admin',
|
|
||||||
type: 'admin',
|
|
||||||
})
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.expect(403);
|
|
||||||
|
|
||||||
await destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Token-admin should not be allowed to create token', async () => {
|
|
||||||
expect.assertions(0);
|
|
||||||
|
|
||||||
const preHook = (app, config, { userService, accessService }) => {
|
|
||||||
app.use('/api/admin/', async (req, res, next) => {
|
|
||||||
const role = await accessService.getPredefinedRole(RoleName.ADMIN);
|
|
||||||
req.user = await userService.createUser({
|
|
||||||
email: 'admin@example.com',
|
|
||||||
rootRole: role.id,
|
|
||||||
});
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const { request, destroy } = await setupAppWithCustomAuth(
|
|
||||||
stores,
|
|
||||||
preHook,
|
|
||||||
undefined,
|
|
||||||
db.rawDatabase,
|
|
||||||
);
|
|
||||||
|
|
||||||
await request
|
|
||||||
.post('/api/admin/api-tokens')
|
|
||||||
.send({
|
|
||||||
username: 'default-admin',
|
|
||||||
type: 'admin',
|
|
||||||
})
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.expect(403);
|
|
||||||
|
|
||||||
await destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('An admin should be forbidden to create an admin token', async () => {
|
|
||||||
const { request, destroy, services } = await setupAppWithAuth(
|
|
||||||
stores,
|
|
||||||
undefined,
|
|
||||||
db.rawDatabase,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { secret } =
|
|
||||||
await services.apiTokenService.createApiTokenWithProjects(
|
|
||||||
{
|
|
||||||
tokenName: 'default-admin',
|
|
||||||
type: ApiTokenType.ADMIN,
|
|
||||||
projects: ['*'],
|
|
||||||
environment: '*',
|
|
||||||
},
|
|
||||||
TEST_AUDIT_USER,
|
|
||||||
);
|
|
||||||
|
|
||||||
await request
|
|
||||||
.post('/api/admin/api-tokens')
|
|
||||||
.send({
|
|
||||||
username: 'default-admin',
|
|
||||||
type: 'admin',
|
|
||||||
})
|
|
||||||
.set('Authorization', secret)
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.expect(403);
|
|
||||||
await destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', async () => {
|
test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', async () => {
|
||||||
expect.assertions(0);
|
expect.assertions(0);
|
||||||
|
|
||||||
@ -312,7 +207,7 @@ test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', asyn
|
|||||||
await request
|
await request
|
||||||
.post('/api/admin/projects/default/api-tokens')
|
.post('/api/admin/projects/default/api-tokens')
|
||||||
.send({
|
.send({
|
||||||
username: 'client-token-maker',
|
tokenName: 'client-token-maker',
|
||||||
type: 'client',
|
type: 'client',
|
||||||
projects: ['default'],
|
projects: ['default'],
|
||||||
})
|
})
|
||||||
@ -374,7 +269,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
await request
|
await request
|
||||||
.post('/api/admin/api-tokens')
|
.post('/api/admin/api-tokens')
|
||||||
.send({
|
.send({
|
||||||
username: 'default-client',
|
tokenName: 'default-client',
|
||||||
type: 'client',
|
type: 'client',
|
||||||
})
|
})
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
@ -431,70 +326,13 @@ describe('Fine grained API token permissions', () => {
|
|||||||
await request
|
await request
|
||||||
.post('/api/admin/api-tokens')
|
.post('/api/admin/api-tokens')
|
||||||
.send({
|
.send({
|
||||||
username: 'default-frontend',
|
tokenName: 'default-frontend',
|
||||||
type: 'frontend',
|
type: 'frontend',
|
||||||
})
|
})
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.expect(403);
|
.expect(403);
|
||||||
await destroy();
|
await destroy();
|
||||||
});
|
});
|
||||||
test('should NOT be allowed to create ADMIN tokens', async () => {
|
|
||||||
const preHook = (
|
|
||||||
app,
|
|
||||||
config,
|
|
||||||
{
|
|
||||||
userService,
|
|
||||||
accessService,
|
|
||||||
}: Pick<IUnleashServices, 'userService' | 'accessService'>,
|
|
||||||
) => {
|
|
||||||
app.use('/api/admin/', async (req, res, next) => {
|
|
||||||
const role = await accessService.getPredefinedRole(
|
|
||||||
RoleName.VIEWER,
|
|
||||||
);
|
|
||||||
const user = await userService.createUser({
|
|
||||||
email: 'mylittlepony_admin@example.com',
|
|
||||||
rootRole: role.id,
|
|
||||||
});
|
|
||||||
req.user = user;
|
|
||||||
const createClientApiTokenRole =
|
|
||||||
await accessService.createRole(
|
|
||||||
{
|
|
||||||
name: 'client_token_creator_cannot_create_admin',
|
|
||||||
description: 'Can create client tokens',
|
|
||||||
permissions: [],
|
|
||||||
type: 'root-custom',
|
|
||||||
createdByUserId: SYSTEM_USER_ID,
|
|
||||||
},
|
|
||||||
SYSTEM_USER_AUDIT,
|
|
||||||
);
|
|
||||||
await accessService.addPermissionToRole(
|
|
||||||
role.id,
|
|
||||||
CREATE_CLIENT_API_TOKEN,
|
|
||||||
);
|
|
||||||
await accessService.addUserToRole(
|
|
||||||
user.id,
|
|
||||||
createClientApiTokenRole.id,
|
|
||||||
'default',
|
|
||||||
);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const { request, destroy } = await setupAppWithCustomAuth(
|
|
||||||
stores,
|
|
||||||
preHook,
|
|
||||||
undefined,
|
|
||||||
db.rawDatabase,
|
|
||||||
);
|
|
||||||
await request
|
|
||||||
.post('/api/admin/api-tokens')
|
|
||||||
.send({
|
|
||||||
username: 'default-admin',
|
|
||||||
type: 'admin',
|
|
||||||
})
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.expect(403);
|
|
||||||
await destroy();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
describe('Read operations', () => {
|
describe('Read operations', () => {
|
||||||
test('READ_FRONTEND_API_TOKEN should be able to see FRONTEND tokens', async () => {
|
test('READ_FRONTEND_API_TOKEN should be able to see FRONTEND tokens', async () => {
|
||||||
@ -546,9 +384,8 @@ describe('Fine grained API token permissions', () => {
|
|||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
|
||||||
|
|
||||||
username: 'client',
|
tokenName: 'client',
|
||||||
secret: '*:environment.client_secret',
|
secret: '*:environment.client_secret',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
});
|
});
|
||||||
@ -556,16 +393,14 @@ describe('Fine grained API token permissions', () => {
|
|||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'admin',
|
||||||
username: 'admin',
|
|
||||||
secret: '*:*.sdfsdf2admin_secret',
|
secret: '*:*.sdfsdf2admin_secret',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
});
|
});
|
||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'frontender',
|
||||||
username: 'frontender',
|
|
||||||
secret: '*:environment:sdfsdf2dfrontend_Secret',
|
secret: '*:environment:sdfsdf2dfrontend_Secret',
|
||||||
type: ApiTokenType.FRONTEND,
|
type: ApiTokenType.FRONTEND,
|
||||||
});
|
});
|
||||||
@ -631,8 +466,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'client',
|
||||||
username: 'client',
|
|
||||||
secret: '*:environment.client_secret_1234',
|
secret: '*:environment.client_secret_1234',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
});
|
});
|
||||||
@ -640,16 +474,14 @@ describe('Fine grained API token permissions', () => {
|
|||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'admin',
|
||||||
username: 'admin',
|
|
||||||
secret: '*:*.admin_secret_1234',
|
secret: '*:*.admin_secret_1234',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
});
|
});
|
||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'frontender',
|
||||||
username: 'frontender',
|
|
||||||
secret: '*:environment.frontend_secret_1234',
|
secret: '*:environment.frontend_secret_1234',
|
||||||
type: ApiTokenType.FRONTEND,
|
type: ApiTokenType.FRONTEND,
|
||||||
});
|
});
|
||||||
@ -693,8 +525,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'client',
|
||||||
username: 'client',
|
|
||||||
secret: '*:environment.client_secret_4321',
|
secret: '*:environment.client_secret_4321',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
});
|
});
|
||||||
@ -702,16 +533,14 @@ describe('Fine grained API token permissions', () => {
|
|||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'admin',
|
||||||
username: 'admin',
|
|
||||||
secret: '*:*.admin_secret_4321',
|
secret: '*:*.admin_secret_4321',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
});
|
});
|
||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'frontender',
|
||||||
username: 'frontender',
|
|
||||||
secret: '*:environment.frontend_secret_4321',
|
secret: '*:environment.frontend_secret_4321',
|
||||||
type: ApiTokenType.FRONTEND,
|
type: ApiTokenType.FRONTEND,
|
||||||
});
|
});
|
||||||
@ -754,24 +583,21 @@ describe('Fine grained API token permissions', () => {
|
|||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'client',
|
||||||
username: 'client',
|
|
||||||
secret: '*:environment.client_secret_4321',
|
secret: '*:environment.client_secret_4321',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
});
|
});
|
||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'admin',
|
||||||
username: 'admin',
|
|
||||||
secret: '*:*.admin_secret_4321',
|
secret: '*:*.admin_secret_4321',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
});
|
});
|
||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'frontender',
|
||||||
username: 'frontender',
|
|
||||||
secret: '*:environment.frontend_secret_4321',
|
secret: '*:environment.frontend_secret_4321',
|
||||||
type: ApiTokenType.FRONTEND,
|
type: ApiTokenType.FRONTEND,
|
||||||
});
|
});
|
||||||
@ -842,8 +668,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
const token = await stores.apiTokenStore.insert({
|
const token = await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'cilent',
|
||||||
username: 'cilent',
|
|
||||||
secret: '*:environment.update_client_token',
|
secret: '*:environment.update_client_token',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
});
|
});
|
||||||
@ -904,8 +729,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
const token = await stores.apiTokenStore.insert({
|
const token = await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'frontend',
|
||||||
username: 'frontend',
|
|
||||||
secret: '*:environment.update_frontend_token',
|
secret: '*:environment.update_frontend_token',
|
||||||
type: ApiTokenType.FRONTEND,
|
type: ApiTokenType.FRONTEND,
|
||||||
});
|
});
|
||||||
@ -966,9 +790,8 @@ describe('Fine grained API token permissions', () => {
|
|||||||
const token = await stores.apiTokenStore.insert({
|
const token = await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
|
||||||
|
|
||||||
username: 'admin',
|
tokenName: 'admin',
|
||||||
secret: '*:*.update_admin_token',
|
secret: '*:*.update_admin_token',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
});
|
});
|
||||||
@ -1032,8 +855,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
const token = await stores.apiTokenStore.insert({
|
const token = await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'cilent',
|
||||||
username: 'cilent',
|
|
||||||
secret: '*:environment.delete_client_token',
|
secret: '*:environment.delete_client_token',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
});
|
});
|
||||||
@ -1094,8 +916,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
const token = await stores.apiTokenStore.insert({
|
const token = await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'frontend',
|
||||||
username: 'frontend',
|
|
||||||
secret: '*:environment.delete_frontend_token',
|
secret: '*:environment.delete_frontend_token',
|
||||||
type: ApiTokenType.FRONTEND,
|
type: ApiTokenType.FRONTEND,
|
||||||
});
|
});
|
||||||
@ -1155,8 +976,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
const token = await stores.apiTokenStore.insert({
|
const token = await stores.apiTokenStore.insert({
|
||||||
environment: '',
|
environment: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: 'admin',
|
||||||
username: 'admin',
|
|
||||||
secret: '*:*:delete_admin_token',
|
secret: '*:*:delete_admin_token',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
});
|
});
|
||||||
|
@ -52,14 +52,13 @@ test('creates new client token', async () => {
|
|||||||
return app.request
|
return app.request
|
||||||
.post('/api/admin/api-tokens')
|
.post('/api/admin/api-tokens')
|
||||||
.send({
|
.send({
|
||||||
username: 'default-client',
|
tokenName: 'default-client',
|
||||||
type: 'client',
|
type: 'client',
|
||||||
})
|
})
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.expect(201)
|
.expect(201)
|
||||||
.expect((res) => {
|
.expect((res) => {
|
||||||
expect(res.body.username).toBe('default-client');
|
expect(res.body.tokenName).toBe('default-client');
|
||||||
expect(res.body.tokenName).toBe(res.body.username);
|
|
||||||
expect(res.body.type).toBe('client');
|
expect(res.body.type).toBe('client');
|
||||||
expect(res.body.createdAt).toBeTruthy();
|
expect(res.body.createdAt).toBeTruthy();
|
||||||
expect(res.body.secret.length > 16).toBe(true);
|
expect(res.body.secret.length > 16).toBe(true);
|
||||||
@ -70,7 +69,6 @@ test('update client token with expiry', async () => {
|
|||||||
const tokenSecret = '*:environment.random-secret-update';
|
const tokenSecret = '*:environment.random-secret-update';
|
||||||
|
|
||||||
await db.stores.apiTokenStore.insert({
|
await db.stores.apiTokenStore.insert({
|
||||||
username: 'test',
|
|
||||||
projects: ['*'],
|
projects: ['*'],
|
||||||
tokenName: 'test_token',
|
tokenName: 'test_token',
|
||||||
secret: tokenSecret,
|
secret: tokenSecret,
|
||||||
@ -104,7 +102,7 @@ test('creates a lot of client tokens', async () => {
|
|||||||
app.request
|
app.request
|
||||||
.post('/api/admin/api-tokens')
|
.post('/api/admin/api-tokens')
|
||||||
.send({
|
.send({
|
||||||
username: 'default-client',
|
tokenName: 'default-client',
|
||||||
type: 'client',
|
type: 'client',
|
||||||
})
|
})
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
@ -138,7 +136,6 @@ test('removes api token', async () => {
|
|||||||
environment: 'development',
|
environment: 'development',
|
||||||
projects: ['*'],
|
projects: ['*'],
|
||||||
tokenName: 'testtoken',
|
tokenName: 'testtoken',
|
||||||
username: 'test',
|
|
||||||
secret: tokenSecret,
|
secret: tokenSecret,
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
});
|
});
|
||||||
@ -161,7 +158,7 @@ test('creates new client token: project & environment defaults to "*"', async ()
|
|||||||
return app.request
|
return app.request
|
||||||
.post('/api/admin/api-tokens')
|
.post('/api/admin/api-tokens')
|
||||||
.send({
|
.send({
|
||||||
username: 'default-client',
|
tokenName: 'default-client',
|
||||||
type: 'client',
|
type: 'client',
|
||||||
})
|
})
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
@ -178,9 +175,9 @@ test('creates new client token with project & environment set', async () => {
|
|||||||
return app.request
|
return app.request
|
||||||
.post('/api/admin/api-tokens')
|
.post('/api/admin/api-tokens')
|
||||||
.send({
|
.send({
|
||||||
username: 'default-client',
|
tokenName: 'default-client',
|
||||||
type: 'client',
|
type: 'client',
|
||||||
project: 'default',
|
projects: ['default'],
|
||||||
environment: DEFAULT_ENV,
|
environment: DEFAULT_ENV,
|
||||||
})
|
})
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
@ -197,7 +194,7 @@ test('should prefix default token with "*:*."', async () => {
|
|||||||
return app.request
|
return app.request
|
||||||
.post('/api/admin/api-tokens')
|
.post('/api/admin/api-tokens')
|
||||||
.send({
|
.send({
|
||||||
username: 'default-client',
|
tokenName: 'default-client',
|
||||||
type: 'client',
|
type: 'client',
|
||||||
})
|
})
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
@ -211,9 +208,9 @@ test('should prefix token with "project:environment."', async () => {
|
|||||||
return app.request
|
return app.request
|
||||||
.post('/api/admin/api-tokens')
|
.post('/api/admin/api-tokens')
|
||||||
.send({
|
.send({
|
||||||
username: 'default-client',
|
tokenName: 'default-client',
|
||||||
type: 'client',
|
type: 'client',
|
||||||
project: 'default',
|
projects: ['default'],
|
||||||
environment: DEFAULT_ENV,
|
environment: DEFAULT_ENV,
|
||||||
})
|
})
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
@ -227,9 +224,9 @@ test('should not create token for invalid projectId', async () => {
|
|||||||
return app.request
|
return app.request
|
||||||
.post('/api/admin/api-tokens')
|
.post('/api/admin/api-tokens')
|
||||||
.send({
|
.send({
|
||||||
username: 'default-client',
|
tokenName: 'default-client',
|
||||||
type: 'client',
|
type: 'client',
|
||||||
project: 'bogus-project-something',
|
projects: ['bogus-project-something'],
|
||||||
})
|
})
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.expect(400)
|
.expect(400)
|
||||||
@ -244,7 +241,7 @@ test('should not create token for invalid environment', async () => {
|
|||||||
return app.request
|
return app.request
|
||||||
.post('/api/admin/api-tokens')
|
.post('/api/admin/api-tokens')
|
||||||
.send({
|
.send({
|
||||||
username: 'default-client',
|
tokenName: 'default-client',
|
||||||
type: 'client',
|
type: 'client',
|
||||||
environment: 'bogus-environment-something',
|
environment: 'bogus-environment-something',
|
||||||
})
|
})
|
||||||
@ -257,22 +254,21 @@ test('should not create token for invalid environment', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('needs one of the username and tokenName properties set', async () => {
|
test('needs tokenName properties set', async () => {
|
||||||
return app.request
|
return app.request
|
||||||
.post('/api/admin/api-tokens')
|
.post('/api/admin/api-tokens')
|
||||||
.send({
|
.send({
|
||||||
type: 'admin',
|
type: 'client',
|
||||||
environment: '*',
|
environment: '*',
|
||||||
})
|
})
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('only one of tokenName and username can be set', async () => {
|
test('can not create token with admin type', async () => {
|
||||||
return app.request
|
return app.request
|
||||||
.post('/api/admin/api-tokens')
|
.post('/api/admin/api-tokens')
|
||||||
.send({
|
.send({
|
||||||
username: 'default-client-name',
|
|
||||||
tokenName: 'default-token-name',
|
tokenName: 'default-token-name',
|
||||||
type: 'admin',
|
type: 'admin',
|
||||||
environment: '*',
|
environment: '*',
|
||||||
@ -285,7 +281,7 @@ test('client tokens cannot span all environments', async () => {
|
|||||||
return app.request
|
return app.request
|
||||||
.post('/api/admin/api-tokens')
|
.post('/api/admin/api-tokens')
|
||||||
.send({
|
.send({
|
||||||
username: 'default-client',
|
tokenName: 'default-client',
|
||||||
type: 'client',
|
type: 'client',
|
||||||
environment: ALL,
|
environment: ALL,
|
||||||
})
|
})
|
||||||
@ -302,9 +298,9 @@ test('should create token for disabled environment', async () => {
|
|||||||
return app.request
|
return app.request
|
||||||
.post('/api/admin/api-tokens')
|
.post('/api/admin/api-tokens')
|
||||||
.send({
|
.send({
|
||||||
username: 'default',
|
tokenName: 'default',
|
||||||
type: 'client',
|
type: 'client',
|
||||||
project: 'default',
|
projects: ['default'],
|
||||||
environment: 'disabledEnvironment',
|
environment: 'disabledEnvironment',
|
||||||
})
|
})
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
|
@ -77,7 +77,7 @@ test('fails to create new client token when given wrong project', async () => {
|
|||||||
return app.request
|
return app.request
|
||||||
.post('/api/admin/projects/wrong/api-tokens')
|
.post('/api/admin/projects/wrong/api-tokens')
|
||||||
.send({
|
.send({
|
||||||
username: 'default-client',
|
tokenName: 'default-client',
|
||||||
type: 'client',
|
type: 'client',
|
||||||
projects: ['wrong'],
|
projects: ['wrong'],
|
||||||
environment: 'default',
|
environment: 'default',
|
||||||
@ -90,7 +90,7 @@ test('creates new client token', async () => {
|
|||||||
return app.request
|
return app.request
|
||||||
.post('/api/admin/projects/default/api-tokens')
|
.post('/api/admin/projects/default/api-tokens')
|
||||||
.send({
|
.send({
|
||||||
username: 'default-client',
|
tokenName: 'default-client',
|
||||||
type: 'client',
|
type: 'client',
|
||||||
projects: ['default'],
|
projects: ['default'],
|
||||||
environment: 'default',
|
environment: 'default',
|
||||||
@ -98,7 +98,7 @@ test('creates new client token', async () => {
|
|||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.expect(201)
|
.expect(201)
|
||||||
.expect((res) => {
|
.expect((res) => {
|
||||||
expect(res.body.username).toBe('default-client');
|
expect(res.body.tokenName).toBe('default-client');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -125,11 +125,11 @@ afterAll(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('returns feature flag with "default" config', async () => {
|
test('returns feature flag with "default" config', async () => {
|
||||||
const token = await apiTokenService.createApiToken({
|
const token = await apiTokenService.createApiTokenWithProjects({
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
tokenName,
|
tokenName,
|
||||||
environment: DEFAULT_ENV,
|
environment: DEFAULT_ENV,
|
||||||
project,
|
projects: [project],
|
||||||
});
|
});
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/client/features')
|
.get('/api/client/features')
|
||||||
@ -147,11 +147,11 @@ test('returns feature flag with "default" config', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('returns feature flag with testing environment config', async () => {
|
test('returns feature flag with testing environment config', async () => {
|
||||||
const token = await apiTokenService.createApiToken({
|
const token = await apiTokenService.createApiTokenWithProjects({
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
tokenName: tokenName,
|
tokenName: tokenName,
|
||||||
environment,
|
environment,
|
||||||
project,
|
projects: [project],
|
||||||
});
|
});
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/client/features')
|
.get('/api/client/features')
|
||||||
@ -173,11 +173,11 @@ test('returns feature flag with testing environment config', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('returns feature flag for project2', async () => {
|
test('returns feature flag for project2', async () => {
|
||||||
const token = await apiTokenService.createApiToken({
|
const token = await apiTokenService.createApiTokenWithProjects({
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
tokenName: tokenName,
|
tokenName: tokenName,
|
||||||
environment,
|
environment,
|
||||||
project: project2,
|
projects: [project2],
|
||||||
});
|
});
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/client/features')
|
.get('/api/client/features')
|
||||||
@ -193,11 +193,11 @@ test('returns feature flag for project2', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('returns feature flag for all projects', async () => {
|
test('returns feature flag for all projects', async () => {
|
||||||
const token = await apiTokenService.createApiToken({
|
const token = await apiTokenService.createApiTokenWithProjects({
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
tokenName: tokenName,
|
tokenName: tokenName,
|
||||||
environment,
|
environment,
|
||||||
project: '*',
|
projects: ['*'],
|
||||||
});
|
});
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/client/features')
|
.get('/api/client/features')
|
||||||
|
@ -133,11 +133,11 @@ afterAll(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('doesnt return feature flags if project deleted', async () => {
|
test('doesnt return feature flags if project deleted', async () => {
|
||||||
const token = await apiTokenService.createApiToken({
|
const token = await apiTokenService.createApiTokenWithProjects({
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
tokenName: deletionTokenName,
|
tokenName: deletionTokenName,
|
||||||
environment,
|
environment,
|
||||||
project: deletionProject,
|
projects: [deletionProject],
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.services.projectService.deleteProject(
|
await app.services.projectService.deleteProject(
|
||||||
|
@ -31,11 +31,11 @@ test('should enrich metrics with environment from api-token', async () => {
|
|||||||
type: 'test',
|
type: 'test',
|
||||||
});
|
});
|
||||||
|
|
||||||
const token = await apiTokenService.createApiToken({
|
const token = await apiTokenService.createApiTokenWithProjects({
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
tokenName: 'test',
|
tokenName: 'test',
|
||||||
environment: 'some',
|
environment: 'some',
|
||||||
project: '*',
|
projects: ['*'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const featureName = Object.keys(metricsExample.bucket.toggles)[0];
|
const featureName = Object.keys(metricsExample.bucket.toggles)[0];
|
||||||
|
@ -74,12 +74,14 @@ test('should accept client metrics', async () => {
|
|||||||
test('should pick up environment from token', async () => {
|
test('should pick up environment from token', async () => {
|
||||||
const environment = 'test';
|
const environment = 'test';
|
||||||
await db.stores.environmentStore.create({ name: 'test', type: 'test' });
|
await db.stores.environmentStore.create({ name: 'test', type: 'test' });
|
||||||
const token = await app.services.apiTokenService.createApiToken({
|
const token = await app.services.apiTokenService.createApiTokenWithProjects(
|
||||||
type: ApiTokenType.CLIENT,
|
{
|
||||||
project: 'default',
|
type: ApiTokenType.CLIENT,
|
||||||
environment,
|
projects: ['default'],
|
||||||
tokenName: 'tester',
|
environment,
|
||||||
});
|
tokenName: 'tester',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// @ts-expect-error - cachedFeatureNames is a private property in ClientMetricsServiceV2
|
// @ts-expect-error - cachedFeatureNames is a private property in ClientMetricsServiceV2
|
||||||
app.services.clientMetricsServiceV2.cachedFeatureNames = vi
|
app.services.clientMetricsServiceV2.cachedFeatureNames = vi
|
||||||
@ -129,12 +131,14 @@ test('should set lastSeen for toggles with metrics both for toggle and toggle en
|
|||||||
.fn<() => Promise<string[]>>()
|
.fn<() => Promise<string[]>>()
|
||||||
.mockResolvedValue(['t1', 't2']);
|
.mockResolvedValue(['t1', 't2']);
|
||||||
|
|
||||||
const token = await app.services.apiTokenService.createApiToken({
|
const token = await app.services.apiTokenService.createApiTokenWithProjects(
|
||||||
type: ApiTokenType.CLIENT,
|
{
|
||||||
project: 'default',
|
type: ApiTokenType.CLIENT,
|
||||||
environment: 'default',
|
projects: ['default'],
|
||||||
tokenName: 'tester',
|
environment: 'default',
|
||||||
});
|
tokenName: 'tester',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await app.request
|
await app.request
|
||||||
.post('/api/client/metrics')
|
.post('/api/client/metrics')
|
||||||
|
@ -68,10 +68,10 @@ test('should have empty list of tokens', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should create client token', async () => {
|
test('should create client token', async () => {
|
||||||
const token = await apiTokenService.createApiToken({
|
const token = await apiTokenService.createApiTokenWithProjects({
|
||||||
tokenName: 'default-client',
|
tokenName: 'default-client',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
project: '*',
|
projects: ['*'],
|
||||||
environment: DEFAULT_ENV,
|
environment: DEFAULT_ENV,
|
||||||
});
|
});
|
||||||
const allTokens = await apiTokenService.getAllTokens();
|
const allTokens = await apiTokenService.getAllTokens();
|
||||||
@ -79,15 +79,15 @@ test('should create client token', async () => {
|
|||||||
expect(allTokens.length).toBe(1);
|
expect(allTokens.length).toBe(1);
|
||||||
expect(token.secret.length > 32).toBe(true);
|
expect(token.secret.length > 32).toBe(true);
|
||||||
expect(token.type).toBe(ApiTokenType.CLIENT);
|
expect(token.type).toBe(ApiTokenType.CLIENT);
|
||||||
expect(token.username).toBe('default-client');
|
expect(token.tokenName).toBe('default-client');
|
||||||
expect(allTokens[0].secret).toBe(token.secret);
|
expect(allTokens[0].secret).toBe(token.secret);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create admin token', async () => {
|
test('should create admin token', async () => {
|
||||||
const token = await apiTokenService.createApiToken({
|
const token = await apiTokenService.createApiTokenWithProjects({
|
||||||
tokenName: 'admin',
|
tokenName: 'admin',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
project: '*',
|
projects: ['*'],
|
||||||
environment: '*',
|
environment: '*',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -97,11 +97,11 @@ test('should create admin token', async () => {
|
|||||||
|
|
||||||
test('should set expiry of token', async () => {
|
test('should set expiry of token', async () => {
|
||||||
const time = new Date('2022-01-01');
|
const time = new Date('2022-01-01');
|
||||||
await apiTokenService.createApiToken({
|
await apiTokenService.createApiTokenWithProjects({
|
||||||
tokenName: 'default-client',
|
tokenName: 'default-client',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
expiresAt: time,
|
expiresAt: time,
|
||||||
project: '*',
|
projects: ['*'],
|
||||||
environment: DEFAULT_ENV,
|
environment: DEFAULT_ENV,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -114,12 +114,12 @@ test('should update expiry of token', async () => {
|
|||||||
const time = new Date('2022-01-01');
|
const time = new Date('2022-01-01');
|
||||||
const newTime = new Date('2023-01-01');
|
const newTime = new Date('2023-01-01');
|
||||||
|
|
||||||
const token = await apiTokenService.createApiToken(
|
const token = await apiTokenService.createApiTokenWithProjects(
|
||||||
{
|
{
|
||||||
tokenName: 'default-client',
|
tokenName: 'default-client',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
expiresAt: time,
|
expiresAt: time,
|
||||||
project: '*',
|
projects: ['*'],
|
||||||
environment: DEFAULT_ENV,
|
environment: DEFAULT_ENV,
|
||||||
},
|
},
|
||||||
TEST_AUDIT_USER,
|
TEST_AUDIT_USER,
|
||||||
@ -133,7 +133,7 @@ test('should update expiry of token', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should create client token with project list', async () => {
|
test('should create client token with project list', async () => {
|
||||||
const token = await apiTokenService.createApiToken({
|
const token = await apiTokenService.createApiTokenWithProjects({
|
||||||
tokenName: 'default-client',
|
tokenName: 'default-client',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
projects: ['default', 'test-project'],
|
projects: ['default', 'test-project'],
|
||||||
@ -145,7 +145,7 @@ test('should create client token with project list', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should strip all other projects if ALL_PROJECTS is present', async () => {
|
test('should strip all other projects if ALL_PROJECTS is present', async () => {
|
||||||
const token = await apiTokenService.createApiToken({
|
const token = await apiTokenService.createApiTokenWithProjects({
|
||||||
tokenName: 'default-client',
|
tokenName: 'default-client',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
projects: ['*', 'default'],
|
projects: ['*', 'default'],
|
||||||
@ -159,21 +159,23 @@ test('should return user with multiple projects', async () => {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const tomorrow = addDays(now, 1);
|
const tomorrow = addDays(now, 1);
|
||||||
|
|
||||||
const { secret: secret1 } = await apiTokenService.createApiToken({
|
const { secret: secret1 } =
|
||||||
tokenName: 'default-valid',
|
await apiTokenService.createApiTokenWithProjects({
|
||||||
type: ApiTokenType.CLIENT,
|
tokenName: 'default-valid',
|
||||||
expiresAt: tomorrow,
|
type: ApiTokenType.CLIENT,
|
||||||
projects: ['test-project', 'default'],
|
expiresAt: tomorrow,
|
||||||
environment: DEFAULT_ENV,
|
projects: ['test-project', 'default'],
|
||||||
});
|
environment: DEFAULT_ENV,
|
||||||
|
});
|
||||||
|
|
||||||
const { secret: secret2 } = await apiTokenService.createApiToken({
|
const { secret: secret2 } =
|
||||||
tokenName: 'default-also-valid',
|
await apiTokenService.createApiTokenWithProjects({
|
||||||
type: ApiTokenType.CLIENT,
|
tokenName: 'default-also-valid',
|
||||||
expiresAt: tomorrow,
|
type: ApiTokenType.CLIENT,
|
||||||
projects: ['test-project'],
|
expiresAt: tomorrow,
|
||||||
environment: DEFAULT_ENV,
|
projects: ['test-project'],
|
||||||
});
|
environment: DEFAULT_ENV,
|
||||||
|
});
|
||||||
|
|
||||||
const multiProjectUser = await apiTokenService.getUserForToken(secret1);
|
const multiProjectUser = await apiTokenService.getUserForToken(secret1);
|
||||||
const singleProjectUser = await apiTokenService.getUserForToken(secret2);
|
const singleProjectUser = await apiTokenService.getUserForToken(secret2);
|
||||||
|
Loading…
Reference in New Issue
Block a user