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 () => {
|
||||
const token = {
|
||||
environment: '*',
|
||||
project: '*',
|
||||
projects: ['*'],
|
||||
secret: '*:*.some-random-string',
|
||||
type: ApiTokenType.ADMIN,
|
||||
tokenName: 'admin',
|
||||
@ -52,7 +52,9 @@ test('should add initApiToken for admin token from options', async () => {
|
||||
expect(config.authentication.initApiTokens[0].environment).toBe(
|
||||
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(
|
||||
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 () => {
|
||||
const token = {
|
||||
environment: 'development',
|
||||
project: 'default',
|
||||
projects: ['default'],
|
||||
secret: 'default:development.some-random-string',
|
||||
type: ApiTokenType.CLIENT,
|
||||
tokenName: 'admin',
|
||||
@ -86,7 +88,9 @@ test('should add initApiToken for client token from options', async () => {
|
||||
expect(config.authentication.initApiTokens[0].environment).toBe(
|
||||
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(
|
||||
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[0].environment).toBe('*');
|
||||
expect(config.authentication.initApiTokens[0].project).toBe('*');
|
||||
expect(config.authentication.initApiTokens[0].projects).toMatchObject([
|
||||
'*',
|
||||
]);
|
||||
expect(config.authentication.initApiTokens[0].type).toBe(
|
||||
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';
|
||||
const token = {
|
||||
environment: '*',
|
||||
project: '*',
|
||||
projects: ['*'],
|
||||
secret: '*:*.some-random-string',
|
||||
type: ApiTokenType.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(
|
||||
'development',
|
||||
);
|
||||
expect(config.authentication.initApiTokens[0].project).toBe('default');
|
||||
expect(config.authentication.initApiTokens[0].projects).toMatchObject([
|
||||
'default',
|
||||
]);
|
||||
expect(config.authentication.initApiTokens[0].type).toBe(
|
||||
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 () => {
|
||||
const token = {
|
||||
environment: '*',
|
||||
project: '*',
|
||||
projects: ['*'],
|
||||
secret: '*:*.some-random-string',
|
||||
type: ApiTokenType.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';
|
||||
const token = {
|
||||
environment: '*',
|
||||
project: '*',
|
||||
projects: ['*'],
|
||||
secret: '*:*.some-random-string',
|
||||
type: ApiTokenType.ADMIN,
|
||||
tokenName: 'admin',
|
||||
|
@ -37,7 +37,7 @@ import {
|
||||
secondsToMilliseconds,
|
||||
} from 'date-fns';
|
||||
import EventEmitter from 'events';
|
||||
import { mapLegacyToken, validateApiToken } from './types/models/api-token.js';
|
||||
import { validateApiToken } from './types/models/api-token.js';
|
||||
import {
|
||||
parseEnvVarBoolean,
|
||||
parseEnvVarJSON,
|
||||
@ -417,13 +417,13 @@ const loadTokensFromString = (
|
||||
const [environment = '*'] = rest.split('.');
|
||||
const token = {
|
||||
createdAt: undefined,
|
||||
project,
|
||||
projects: [project],
|
||||
environment,
|
||||
secret,
|
||||
type: tokenType,
|
||||
tokenName: 'admin',
|
||||
};
|
||||
validateApiToken(mapLegacyToken(token));
|
||||
validateApiToken(token);
|
||||
return token;
|
||||
});
|
||||
return tokens;
|
||||
|
@ -50,7 +50,6 @@ const tokenRowReducer = (acc, tokenRow) => {
|
||||
createdAt: token.created_at,
|
||||
alias: token.alias,
|
||||
seenAt: token.seen_at,
|
||||
username: token.token_name ? token.token_name : token.username,
|
||||
};
|
||||
}
|
||||
const currentToken = acc[tokenRow.secret];
|
||||
@ -65,8 +64,8 @@ const tokenRowReducer = (acc, tokenRow) => {
|
||||
};
|
||||
|
||||
const toRow = (newToken: IApiTokenCreate) => ({
|
||||
username: newToken.tokenName ?? newToken.username,
|
||||
token_name: newToken.tokenName ?? newToken.username,
|
||||
username: newToken.tokenName,
|
||||
token_name: newToken.tokenName,
|
||||
secret: newToken.secret,
|
||||
type: newToken.type,
|
||||
environment:
|
||||
@ -192,7 +191,6 @@ export class ApiTokenStore implements IApiTokenStore {
|
||||
await Promise.all(updateProjectTasks);
|
||||
return {
|
||||
...newToken,
|
||||
username: newToken.tokenName,
|
||||
alias: newToken.alias || null,
|
||||
project: newToken.projects?.join(',') || '*',
|
||||
createdAt: row.created_at,
|
||||
|
@ -2436,11 +2436,11 @@ test('should also delete api tokens that were only bound to deleted project', as
|
||||
auditUser,
|
||||
);
|
||||
|
||||
const token = await apiTokenService.createApiToken({
|
||||
const token = await apiTokenService.createApiTokenWithProjects({
|
||||
type: ApiTokenType.CLIENT,
|
||||
tokenName,
|
||||
environment: DEFAULT_ENV,
|
||||
project: project,
|
||||
projects: [project],
|
||||
});
|
||||
|
||||
await projectService.deleteProject(project, user, auditUser);
|
||||
@ -2471,7 +2471,7 @@ test('should not delete project-bound api tokens still bound to project', async
|
||||
auditUser,
|
||||
);
|
||||
|
||||
const token = await apiTokenService.createApiToken({
|
||||
const token = await apiTokenService.createApiTokenWithProjects({
|
||||
type: ApiTokenType.CLIENT,
|
||||
tokenName,
|
||||
environment: DEFAULT_ENV,
|
||||
@ -2507,7 +2507,7 @@ test('should delete project-bound api tokens when all projects they belong to ar
|
||||
auditUser,
|
||||
);
|
||||
|
||||
const token = await apiTokenService.createApiToken({
|
||||
const token = await apiTokenService.createApiTokenWithProjects({
|
||||
type: ApiTokenType.CLIENT,
|
||||
tokenName,
|
||||
environment: DEFAULT_ENV,
|
||||
|
@ -4,7 +4,6 @@ import type { ApiTokenSchema } from './api-token-schema.js';
|
||||
|
||||
const defaultData: ApiTokenSchema = {
|
||||
secret: '',
|
||||
username: '',
|
||||
tokenName: '',
|
||||
type: ApiTokenType.CLIENT,
|
||||
environment: '',
|
||||
|
@ -5,14 +5,7 @@ export const apiTokenSchema = {
|
||||
$id: '#/components/schemas/apiTokenSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: [
|
||||
'secret',
|
||||
'tokenName',
|
||||
'type',
|
||||
'project',
|
||||
'projects',
|
||||
'createdAt',
|
||||
],
|
||||
required: ['secret', 'tokenName', 'type', 'projects', 'createdAt'],
|
||||
description:
|
||||
'An overview of an [Unleash API token](https://docs.getunleash.io/reference/api-tokens-and-client-keys).',
|
||||
properties: {
|
||||
@ -21,13 +14,6 @@ export const apiTokenSchema = {
|
||||
description: 'The token used for authentication.',
|
||||
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: {
|
||||
type: 'string',
|
||||
description: 'A unique name for this particular token',
|
||||
|
@ -1,17 +1,5 @@
|
||||
import type { FromSchema } from 'json-schema-to-ts';
|
||||
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 = {
|
||||
type: 'object',
|
||||
@ -25,20 +13,6 @@ const tokenNameSchema = {
|
||||
},
|
||||
} 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 = {
|
||||
required: ['type'],
|
||||
type: 'object',
|
||||
@ -100,12 +74,7 @@ export const createApiTokenSchema = {
|
||||
type: 'object',
|
||||
description:
|
||||
'The data required to create an [Unleash API token](https://docs.getunleash.io/reference/api-tokens-and-client-keys).',
|
||||
oneOf: [
|
||||
mergeAllOfs([expireSchema, adminSchema, tokenNameSchema]),
|
||||
mergeAllOfs([expireSchema, adminSchema, usernameSchema]),
|
||||
mergeAllOfs([expireSchema, clientFrontendSchema, tokenNameSchema]),
|
||||
mergeAllOfs([expireSchema, clientFrontendSchema, usernameSchema]),
|
||||
],
|
||||
oneOf: [mergeAllOfs([expireSchema, clientFrontendSchema, tokenNameSchema])],
|
||||
components: {},
|
||||
} 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-invited-user-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-variant-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';
|
||||
import type { FrontendApiService } from '../../features/frontend-api/frontend-api-service.js';
|
||||
import { OperationDeniedError } from '../../error/index.js';
|
||||
import type { CreateApiTokenSchema } from '../../internals.js';
|
||||
|
||||
interface TokenParam {
|
||||
token: string;
|
||||
@ -299,24 +300,20 @@ export class ApiTokenController extends Controller {
|
||||
}
|
||||
|
||||
async createApiToken(
|
||||
req: IAuthRequest,
|
||||
req: IAuthRequest<CreateApiTokenSchema>,
|
||||
res: Response<ApiTokenSchema>,
|
||||
): Promise<any> {
|
||||
const createToken = await createApiToken.validateAsync(req.body);
|
||||
const permissionRequired = tokenTypeToCreatePermission(
|
||||
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(
|
||||
req.user,
|
||||
permissionRequired,
|
||||
);
|
||||
if (hasPermission) {
|
||||
const token = await this.apiTokenService.createApiToken(
|
||||
const token = await this.apiTokenService.createApiTokenWithProjects(
|
||||
createToken,
|
||||
req.audit,
|
||||
);
|
||||
|
@ -32,8 +32,9 @@ import Controller from '../../controller.js';
|
||||
import type { Logger } from '../../../logger.js';
|
||||
import type { Response } from 'express';
|
||||
import { timingSafeEqual } from 'crypto';
|
||||
import { createApiToken } from '../../../schema/api-token-schema.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 {
|
||||
token: string;
|
||||
@ -109,7 +110,9 @@ export class ProjectApiTokenController extends Controller {
|
||||
openApiService.validPath({
|
||||
tags: ['Projects'],
|
||||
operationId: 'createProjectApiToken',
|
||||
requestBody: createRequestSchema('createApiTokenSchema'),
|
||||
requestBody: createRequestSchema(
|
||||
'createProjectApiTokenSchema',
|
||||
),
|
||||
summary: 'Create a project API token.',
|
||||
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.',
|
||||
@ -160,10 +163,10 @@ export class ProjectApiTokenController extends Controller {
|
||||
}
|
||||
|
||||
async createProjectApiToken(
|
||||
req: IAuthRequest,
|
||||
req: IAuthRequest<{ projectId: string }, CreateProjectApiTokenSchema>,
|
||||
res: Response<ApiTokenSchema>,
|
||||
): Promise<any> {
|
||||
const createToken = await createApiToken.validateAsync(req.body);
|
||||
const createToken = await createProjectApiToken.validateAsync(req.body);
|
||||
const { projectId } = req.params;
|
||||
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]`,
|
||||
);
|
||||
}
|
||||
if (!createToken.project) {
|
||||
createToken.project = projectId;
|
||||
}
|
||||
|
||||
if (
|
||||
createToken.projects.length === 1 &&
|
||||
createToken.projects[0] === projectId
|
||||
) {
|
||||
const token = await this.apiTokenService.createApiToken(
|
||||
createToken,
|
||||
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);
|
||||
}
|
||||
const token = await this.apiTokenService.createApiTokenWithProjects(
|
||||
{ ...createToken, projects: [projectId] },
|
||||
req.audit,
|
||||
);
|
||||
this.openApiService.respondWithValidation(
|
||||
201,
|
||||
res,
|
||||
apiTokenSchema.$id,
|
||||
serializeDates(token),
|
||||
{ location: `api-tokens` },
|
||||
);
|
||||
}
|
||||
|
||||
async deleteProjectApiToken(
|
||||
|
@ -1,51 +1,50 @@
|
||||
import { ALL } from '../types/models/api-token.js';
|
||||
import { createApiToken } from './api-token-schema.js';
|
||||
|
||||
test('should reject token with projects and project', async () => {
|
||||
expect.assertions(1);
|
||||
test('should ignore token extra project field', async () => {
|
||||
expect.assertions(0);
|
||||
try {
|
||||
await createApiToken.validateAsync({
|
||||
username: 'test',
|
||||
type: 'admin',
|
||||
tokenName: 'test',
|
||||
type: 'client',
|
||||
project: 'default',
|
||||
projects: ['default'],
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.details[0].message).toEqual(
|
||||
'"project" must not exist simultaneously with [projects]',
|
||||
);
|
||||
expect(error).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
test('should not have default project set if projects is present', async () => {
|
||||
const token = await createApiToken.validateAsync({
|
||||
username: 'test',
|
||||
type: 'admin',
|
||||
tokenName: 'test',
|
||||
type: 'client',
|
||||
projects: ['default'],
|
||||
});
|
||||
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({
|
||||
username: 'test',
|
||||
type: 'admin',
|
||||
tokenName: 'test',
|
||||
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({
|
||||
username: 'test',
|
||||
type: 'admin',
|
||||
tokenName: 'test',
|
||||
type: 'client',
|
||||
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 () => {
|
||||
const token = await createApiToken.validateAsync({
|
||||
username: 'test',
|
||||
tokenName: 'test',
|
||||
type: 'frontend',
|
||||
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 () => {
|
||||
const token = await createApiToken.validateAsync({
|
||||
username: 'test',
|
||||
tokenName: 'test',
|
||||
type: 'frontend',
|
||||
project: 'default',
|
||||
});
|
||||
expect(token.environment).toEqual('default');
|
||||
});
|
||||
|
@ -6,29 +6,18 @@ import { DEFAULT_ENV } from '../util/constants.js';
|
||||
export const createApiToken = joi
|
||||
.object()
|
||||
.keys({
|
||||
username: joi.string().optional(),
|
||||
tokenName: joi.string().optional(),
|
||||
tokenName: joi.string().required(),
|
||||
type: joi
|
||||
.string()
|
||||
.lowercase()
|
||||
.required()
|
||||
.valid(
|
||||
ApiTokenType.ADMIN,
|
||||
ApiTokenType.CLIENT,
|
||||
ApiTokenType.FRONTEND,
|
||||
),
|
||||
.valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND),
|
||||
expiresAt: joi.date().optional(),
|
||||
project: joi.when('projects', {
|
||||
not: joi.required(),
|
||||
then: joi.string().optional().default(ALL),
|
||||
}),
|
||||
projects: joi.array().min(0).optional(),
|
||||
projects: joi.array().min(1).optional().default([ALL]),
|
||||
environment: joi.when('type', {
|
||||
is: joi.string().valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND),
|
||||
then: joi.string().optional().default(DEFAULT_ENV),
|
||||
otherwise: joi.string().optional().default(ALL),
|
||||
}),
|
||||
})
|
||||
.nand('username', 'tokenName')
|
||||
.nand('project', 'projects')
|
||||
.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 () => {
|
||||
const token = {
|
||||
environment: '*',
|
||||
project: '*',
|
||||
projects: ['*'],
|
||||
secret: '*:*:some-random-string',
|
||||
type: ApiTokenType.ADMIN,
|
||||
tokenName: 'admin',
|
||||
|
@ -5,11 +5,9 @@ import type { IUnleashStores } from '../types/stores.js';
|
||||
import type { IUnleashConfig } from '../types/option.js';
|
||||
import ApiUser, { type IApiUser } from '../types/api-user.js';
|
||||
import {
|
||||
type ILegacyApiTokenCreate,
|
||||
resolveValidProjects,
|
||||
validateApiToken,
|
||||
validateApiTokenEnvironment,
|
||||
mapLegacyToken,
|
||||
mapLegacyTokenWithSecret,
|
||||
} from '../types/models/api-token.js';
|
||||
import type { IApiTokenStore } from '../types/stores/api-token-store.js';
|
||||
import { FOREIGN_KEY_VIOLATION } from '../error/db-error.js';
|
||||
@ -194,7 +192,7 @@ export class ApiTokenService {
|
||||
return this.store.getAll();
|
||||
}
|
||||
|
||||
async initApiTokens(tokens: ILegacyApiTokenCreate[]) {
|
||||
async initApiTokens(tokens: IApiTokenCreate[]) {
|
||||
const tokenCount = await this.store.count();
|
||||
if (tokenCount > 0) {
|
||||
this.logger.debug(
|
||||
@ -203,9 +201,9 @@ export class ApiTokenService {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const createAll = tokens
|
||||
.map(mapLegacyTokenWithSecret)
|
||||
.map((t) => this.insertNewApiToken(t, SYSTEM_USER_AUDIT));
|
||||
const createAll = tokens.map((t) =>
|
||||
this.insertNewApiToken(t, SYSTEM_USER_AUDIT),
|
||||
);
|
||||
await Promise.all(createAll);
|
||||
this.logger.info(
|
||||
`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 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'>,
|
||||
auditUser: IAuditUser = SYSTEM_USER_AUDIT,
|
||||
): Promise<IApiToken> {
|
||||
return this.internalCreateApiTokenWithProjects(newToken, auditUser);
|
||||
return this.internalCreateApiTokenWithProjects(
|
||||
{
|
||||
...newToken,
|
||||
projects: resolveValidProjects(newToken.projects),
|
||||
},
|
||||
auditUser,
|
||||
);
|
||||
}
|
||||
|
||||
private async internalCreateApiTokenWithProjects(
|
||||
|
@ -210,10 +210,6 @@ export interface IApiTokenCreate {
|
||||
environment: string;
|
||||
projects: string[];
|
||||
expiresAt?: Date;
|
||||
/**
|
||||
* @deprecated Use tokenName instead
|
||||
*/
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface IApiToken extends Omit<IApiTokenCreate, 'alias'> {
|
||||
|
@ -4,64 +4,16 @@ import { ApiTokenType } from '../model.js';
|
||||
|
||||
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 => {
|
||||
return projects && projects.length === 1 && projects[0] === ALL;
|
||||
};
|
||||
|
||||
export const mapLegacyProjects = (
|
||||
project?: string,
|
||||
projects?: string[],
|
||||
): 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',
|
||||
);
|
||||
export const resolveValidProjects = (projects: string[]): string[] => {
|
||||
if (projects.includes('*')) {
|
||||
return ['*'];
|
||||
}
|
||||
return cleanedProjects;
|
||||
};
|
||||
|
||||
export const mapLegacyToken = (
|
||||
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,
|
||||
};
|
||||
return projects;
|
||||
};
|
||||
|
||||
export const validateApiToken = ({
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { Express } from 'express';
|
||||
import type EventEmitter from 'events';
|
||||
import type { LogLevel, LogProvider } from '../logger.js';
|
||||
import type { ILegacyApiTokenCreate } from './models/api-token.js';
|
||||
import type { IApiTokenCreate } from './model.js';
|
||||
import type {
|
||||
IExperimentalOptions,
|
||||
IFlagContext,
|
||||
@ -85,7 +85,7 @@ export interface IAuthOption {
|
||||
customAuthHandler?: CustomAuthHandler;
|
||||
createAdminUser?: boolean;
|
||||
initialAdminUser?: UsernameAdminUser;
|
||||
initApiTokens: ILegacyApiTokenCreate[];
|
||||
initApiTokens: IApiTokenCreate[];
|
||||
}
|
||||
|
||||
export interface IImportOption {
|
||||
|
@ -63,7 +63,7 @@ process.nextTick(async () => {
|
||||
initApiTokens: [
|
||||
{
|
||||
environment: '*',
|
||||
project: '*',
|
||||
projects: ['*'],
|
||||
secret: '*:*.964a287e1b728cb5f4f3e0120df92cb5',
|
||||
type: ApiTokenType.ADMIN,
|
||||
tokenName: 'some-user',
|
||||
|
@ -1,7 +1,4 @@
|
||||
import {
|
||||
setupAppWithAuth,
|
||||
setupAppWithCustomAuth,
|
||||
} from '../../helpers/test-helper.js';
|
||||
import { setupAppWithCustomAuth } from '../../helpers/test-helper.js';
|
||||
import dbInit, { type ITestDb } from '../../helpers/database-init.js';
|
||||
import getLogger from '../../../fixtures/no-logger.js';
|
||||
import { ApiTokenType } from '../../../../lib/types/model.js';
|
||||
@ -16,7 +13,6 @@ import {
|
||||
SYSTEM_USER,
|
||||
SYSTEM_USER_AUDIT,
|
||||
SYSTEM_USER_ID,
|
||||
TEST_AUDIT_USER,
|
||||
UPDATE_CLIENT_API_TOKEN,
|
||||
} from '../../../../lib/types/index.js';
|
||||
import { addDays } from 'date-fns';
|
||||
@ -79,8 +75,7 @@ test('editor users should only get client or frontend tokens', async () => {
|
||||
await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'test',
|
||||
tokenName: 'test',
|
||||
secret: '*:environment.1234',
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
@ -88,8 +83,7 @@ test('editor users should only get client or frontend tokens', async () => {
|
||||
await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'frontend',
|
||||
tokenName: 'frontend',
|
||||
secret: '*:environment.12345',
|
||||
type: ApiTokenType.FRONTEND,
|
||||
});
|
||||
@ -97,8 +91,7 @@ test('editor users should only get client or frontend tokens', async () => {
|
||||
await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'test',
|
||||
tokenName: 'test',
|
||||
secret: '*:*.sdfsdf2d',
|
||||
type: ApiTokenType.ADMIN,
|
||||
});
|
||||
@ -141,8 +134,7 @@ test('viewer users should not be allowed to fetch tokens', async () => {
|
||||
await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'test',
|
||||
tokenName: 'test',
|
||||
secret: '*:environment.1234',
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
@ -150,8 +142,7 @@ test('viewer users should not be allowed to fetch tokens', async () => {
|
||||
await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'test',
|
||||
tokenName: 'test',
|
||||
secret: '*:*.sdfsdf2d',
|
||||
type: ApiTokenType.ADMIN,
|
||||
});
|
||||
@ -164,102 +155,6 @@ test('viewer users should not be allowed to fetch tokens', async () => {
|
||||
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 () => {
|
||||
expect.assertions(0);
|
||||
|
||||
@ -312,7 +207,7 @@ test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', asyn
|
||||
await request
|
||||
.post('/api/admin/projects/default/api-tokens')
|
||||
.send({
|
||||
username: 'client-token-maker',
|
||||
tokenName: 'client-token-maker',
|
||||
type: 'client',
|
||||
projects: ['default'],
|
||||
})
|
||||
@ -374,7 +269,7 @@ describe('Fine grained API token permissions', () => {
|
||||
await request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
tokenName: 'default-client',
|
||||
type: 'client',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
@ -431,70 +326,13 @@ describe('Fine grained API token permissions', () => {
|
||||
await request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-frontend',
|
||||
tokenName: 'default-frontend',
|
||||
type: 'frontend',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(403);
|
||||
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', () => {
|
||||
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({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
|
||||
username: 'client',
|
||||
tokenName: 'client',
|
||||
secret: '*:environment.client_secret',
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
@ -556,16 +393,14 @@ describe('Fine grained API token permissions', () => {
|
||||
await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'admin',
|
||||
tokenName: 'admin',
|
||||
secret: '*:*.sdfsdf2admin_secret',
|
||||
type: ApiTokenType.ADMIN,
|
||||
});
|
||||
await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'frontender',
|
||||
tokenName: 'frontender',
|
||||
secret: '*:environment:sdfsdf2dfrontend_Secret',
|
||||
type: ApiTokenType.FRONTEND,
|
||||
});
|
||||
@ -631,8 +466,7 @@ describe('Fine grained API token permissions', () => {
|
||||
await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'client',
|
||||
tokenName: 'client',
|
||||
secret: '*:environment.client_secret_1234',
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
@ -640,16 +474,14 @@ describe('Fine grained API token permissions', () => {
|
||||
await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'admin',
|
||||
tokenName: 'admin',
|
||||
secret: '*:*.admin_secret_1234',
|
||||
type: ApiTokenType.ADMIN,
|
||||
});
|
||||
await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'frontender',
|
||||
tokenName: 'frontender',
|
||||
secret: '*:environment.frontend_secret_1234',
|
||||
type: ApiTokenType.FRONTEND,
|
||||
});
|
||||
@ -693,8 +525,7 @@ describe('Fine grained API token permissions', () => {
|
||||
await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'client',
|
||||
tokenName: 'client',
|
||||
secret: '*:environment.client_secret_4321',
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
@ -702,16 +533,14 @@ describe('Fine grained API token permissions', () => {
|
||||
await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'admin',
|
||||
tokenName: 'admin',
|
||||
secret: '*:*.admin_secret_4321',
|
||||
type: ApiTokenType.ADMIN,
|
||||
});
|
||||
await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'frontender',
|
||||
tokenName: 'frontender',
|
||||
secret: '*:environment.frontend_secret_4321',
|
||||
type: ApiTokenType.FRONTEND,
|
||||
});
|
||||
@ -754,24 +583,21 @@ describe('Fine grained API token permissions', () => {
|
||||
await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'client',
|
||||
tokenName: 'client',
|
||||
secret: '*:environment.client_secret_4321',
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'admin',
|
||||
tokenName: 'admin',
|
||||
secret: '*:*.admin_secret_4321',
|
||||
type: ApiTokenType.ADMIN,
|
||||
});
|
||||
await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'frontender',
|
||||
tokenName: 'frontender',
|
||||
secret: '*:environment.frontend_secret_4321',
|
||||
type: ApiTokenType.FRONTEND,
|
||||
});
|
||||
@ -842,8 +668,7 @@ describe('Fine grained API token permissions', () => {
|
||||
const token = await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'cilent',
|
||||
tokenName: 'cilent',
|
||||
secret: '*:environment.update_client_token',
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
@ -904,8 +729,7 @@ describe('Fine grained API token permissions', () => {
|
||||
const token = await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'frontend',
|
||||
tokenName: 'frontend',
|
||||
secret: '*:environment.update_frontend_token',
|
||||
type: ApiTokenType.FRONTEND,
|
||||
});
|
||||
@ -966,9 +790,8 @@ describe('Fine grained API token permissions', () => {
|
||||
const token = await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
|
||||
username: 'admin',
|
||||
tokenName: 'admin',
|
||||
secret: '*:*.update_admin_token',
|
||||
type: ApiTokenType.ADMIN,
|
||||
});
|
||||
@ -1032,8 +855,7 @@ describe('Fine grained API token permissions', () => {
|
||||
const token = await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'cilent',
|
||||
tokenName: 'cilent',
|
||||
secret: '*:environment.delete_client_token',
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
@ -1094,8 +916,7 @@ describe('Fine grained API token permissions', () => {
|
||||
const token = await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'frontend',
|
||||
tokenName: 'frontend',
|
||||
secret: '*:environment.delete_frontend_token',
|
||||
type: ApiTokenType.FRONTEND,
|
||||
});
|
||||
@ -1155,8 +976,7 @@ describe('Fine grained API token permissions', () => {
|
||||
const token = await stores.apiTokenStore.insert({
|
||||
environment: '',
|
||||
projects: [],
|
||||
tokenName: '',
|
||||
username: 'admin',
|
||||
tokenName: 'admin',
|
||||
secret: '*:*:delete_admin_token',
|
||||
type: ApiTokenType.ADMIN,
|
||||
});
|
||||
|
@ -52,14 +52,13 @@ test('creates new client token', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
tokenName: 'default-client',
|
||||
type: 'client',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201)
|
||||
.expect((res) => {
|
||||
expect(res.body.username).toBe('default-client');
|
||||
expect(res.body.tokenName).toBe(res.body.username);
|
||||
expect(res.body.tokenName).toBe('default-client');
|
||||
expect(res.body.type).toBe('client');
|
||||
expect(res.body.createdAt).toBeTruthy();
|
||||
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';
|
||||
|
||||
await db.stores.apiTokenStore.insert({
|
||||
username: 'test',
|
||||
projects: ['*'],
|
||||
tokenName: 'test_token',
|
||||
secret: tokenSecret,
|
||||
@ -104,7 +102,7 @@ test('creates a lot of client tokens', async () => {
|
||||
app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
tokenName: 'default-client',
|
||||
type: 'client',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
@ -138,7 +136,6 @@ test('removes api token', async () => {
|
||||
environment: 'development',
|
||||
projects: ['*'],
|
||||
tokenName: 'testtoken',
|
||||
username: 'test',
|
||||
secret: tokenSecret,
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
@ -161,7 +158,7 @@ test('creates new client token: project & environment defaults to "*"', async ()
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
tokenName: 'default-client',
|
||||
type: 'client',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
@ -178,9 +175,9 @@ test('creates new client token with project & environment set', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
tokenName: 'default-client',
|
||||
type: 'client',
|
||||
project: 'default',
|
||||
projects: ['default'],
|
||||
environment: DEFAULT_ENV,
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
@ -197,7 +194,7 @@ test('should prefix default token with "*:*."', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
tokenName: 'default-client',
|
||||
type: 'client',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
@ -211,9 +208,9 @@ test('should prefix token with "project:environment."', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
tokenName: 'default-client',
|
||||
type: 'client',
|
||||
project: 'default',
|
||||
projects: ['default'],
|
||||
environment: DEFAULT_ENV,
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
@ -227,9 +224,9 @@ test('should not create token for invalid projectId', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
tokenName: 'default-client',
|
||||
type: 'client',
|
||||
project: 'bogus-project-something',
|
||||
projects: ['bogus-project-something'],
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(400)
|
||||
@ -244,7 +241,7 @@ test('should not create token for invalid environment', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
tokenName: 'default-client',
|
||||
type: 'client',
|
||||
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
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
type: 'admin',
|
||||
type: 'client',
|
||||
environment: '*',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.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
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client-name',
|
||||
tokenName: 'default-token-name',
|
||||
type: 'admin',
|
||||
environment: '*',
|
||||
@ -285,7 +281,7 @@ test('client tokens cannot span all environments', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
tokenName: 'default-client',
|
||||
type: 'client',
|
||||
environment: ALL,
|
||||
})
|
||||
@ -302,9 +298,9 @@ test('should create token for disabled environment', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default',
|
||||
tokenName: 'default',
|
||||
type: 'client',
|
||||
project: 'default',
|
||||
projects: ['default'],
|
||||
environment: 'disabledEnvironment',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
|
@ -77,7 +77,7 @@ test('fails to create new client token when given wrong project', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/projects/wrong/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
tokenName: 'default-client',
|
||||
type: 'client',
|
||||
projects: ['wrong'],
|
||||
environment: 'default',
|
||||
@ -90,7 +90,7 @@ test('creates new client token', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/projects/default/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
tokenName: 'default-client',
|
||||
type: 'client',
|
||||
projects: ['default'],
|
||||
environment: 'default',
|
||||
@ -98,7 +98,7 @@ test('creates new client token', async () => {
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201)
|
||||
.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 () => {
|
||||
const token = await apiTokenService.createApiToken({
|
||||
const token = await apiTokenService.createApiTokenWithProjects({
|
||||
type: ApiTokenType.CLIENT,
|
||||
tokenName,
|
||||
environment: DEFAULT_ENV,
|
||||
project,
|
||||
projects: [project],
|
||||
});
|
||||
await app.request
|
||||
.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 () => {
|
||||
const token = await apiTokenService.createApiToken({
|
||||
const token = await apiTokenService.createApiTokenWithProjects({
|
||||
type: ApiTokenType.CLIENT,
|
||||
tokenName: tokenName,
|
||||
environment,
|
||||
project,
|
||||
projects: [project],
|
||||
});
|
||||
await app.request
|
||||
.get('/api/client/features')
|
||||
@ -173,11 +173,11 @@ test('returns feature flag with testing environment config', async () => {
|
||||
});
|
||||
|
||||
test('returns feature flag for project2', async () => {
|
||||
const token = await apiTokenService.createApiToken({
|
||||
const token = await apiTokenService.createApiTokenWithProjects({
|
||||
type: ApiTokenType.CLIENT,
|
||||
tokenName: tokenName,
|
||||
environment,
|
||||
project: project2,
|
||||
projects: [project2],
|
||||
});
|
||||
await app.request
|
||||
.get('/api/client/features')
|
||||
@ -193,11 +193,11 @@ test('returns feature flag for project2', async () => {
|
||||
});
|
||||
|
||||
test('returns feature flag for all projects', async () => {
|
||||
const token = await apiTokenService.createApiToken({
|
||||
const token = await apiTokenService.createApiTokenWithProjects({
|
||||
type: ApiTokenType.CLIENT,
|
||||
tokenName: tokenName,
|
||||
environment,
|
||||
project: '*',
|
||||
projects: ['*'],
|
||||
});
|
||||
await app.request
|
||||
.get('/api/client/features')
|
||||
|
@ -133,11 +133,11 @@ afterAll(async () => {
|
||||
});
|
||||
|
||||
test('doesnt return feature flags if project deleted', async () => {
|
||||
const token = await apiTokenService.createApiToken({
|
||||
const token = await apiTokenService.createApiTokenWithProjects({
|
||||
type: ApiTokenType.CLIENT,
|
||||
tokenName: deletionTokenName,
|
||||
environment,
|
||||
project: deletionProject,
|
||||
projects: [deletionProject],
|
||||
});
|
||||
|
||||
await app.services.projectService.deleteProject(
|
||||
|
@ -31,11 +31,11 @@ test('should enrich metrics with environment from api-token', async () => {
|
||||
type: 'test',
|
||||
});
|
||||
|
||||
const token = await apiTokenService.createApiToken({
|
||||
const token = await apiTokenService.createApiTokenWithProjects({
|
||||
type: ApiTokenType.CLIENT,
|
||||
tokenName: 'test',
|
||||
environment: 'some',
|
||||
project: '*',
|
||||
projects: ['*'],
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const environment = 'test';
|
||||
await db.stores.environmentStore.create({ name: 'test', type: 'test' });
|
||||
const token = await app.services.apiTokenService.createApiToken({
|
||||
type: ApiTokenType.CLIENT,
|
||||
project: 'default',
|
||||
environment,
|
||||
tokenName: 'tester',
|
||||
});
|
||||
const token = await app.services.apiTokenService.createApiTokenWithProjects(
|
||||
{
|
||||
type: ApiTokenType.CLIENT,
|
||||
projects: ['default'],
|
||||
environment,
|
||||
tokenName: 'tester',
|
||||
},
|
||||
);
|
||||
|
||||
// @ts-expect-error - cachedFeatureNames is a private property in ClientMetricsServiceV2
|
||||
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[]>>()
|
||||
.mockResolvedValue(['t1', 't2']);
|
||||
|
||||
const token = await app.services.apiTokenService.createApiToken({
|
||||
type: ApiTokenType.CLIENT,
|
||||
project: 'default',
|
||||
environment: 'default',
|
||||
tokenName: 'tester',
|
||||
});
|
||||
const token = await app.services.apiTokenService.createApiTokenWithProjects(
|
||||
{
|
||||
type: ApiTokenType.CLIENT,
|
||||
projects: ['default'],
|
||||
environment: 'default',
|
||||
tokenName: 'tester',
|
||||
},
|
||||
);
|
||||
|
||||
await app.request
|
||||
.post('/api/client/metrics')
|
||||
|
@ -68,10 +68,10 @@ test('should have empty list of tokens', async () => {
|
||||
});
|
||||
|
||||
test('should create client token', async () => {
|
||||
const token = await apiTokenService.createApiToken({
|
||||
const token = await apiTokenService.createApiTokenWithProjects({
|
||||
tokenName: 'default-client',
|
||||
type: ApiTokenType.CLIENT,
|
||||
project: '*',
|
||||
projects: ['*'],
|
||||
environment: DEFAULT_ENV,
|
||||
});
|
||||
const allTokens = await apiTokenService.getAllTokens();
|
||||
@ -79,15 +79,15 @@ test('should create client token', async () => {
|
||||
expect(allTokens.length).toBe(1);
|
||||
expect(token.secret.length > 32).toBe(true);
|
||||
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);
|
||||
});
|
||||
|
||||
test('should create admin token', async () => {
|
||||
const token = await apiTokenService.createApiToken({
|
||||
const token = await apiTokenService.createApiTokenWithProjects({
|
||||
tokenName: 'admin',
|
||||
type: ApiTokenType.ADMIN,
|
||||
project: '*',
|
||||
projects: ['*'],
|
||||
environment: '*',
|
||||
});
|
||||
|
||||
@ -97,11 +97,11 @@ test('should create admin token', async () => {
|
||||
|
||||
test('should set expiry of token', async () => {
|
||||
const time = new Date('2022-01-01');
|
||||
await apiTokenService.createApiToken({
|
||||
await apiTokenService.createApiTokenWithProjects({
|
||||
tokenName: 'default-client',
|
||||
type: ApiTokenType.CLIENT,
|
||||
expiresAt: time,
|
||||
project: '*',
|
||||
projects: ['*'],
|
||||
environment: DEFAULT_ENV,
|
||||
});
|
||||
|
||||
@ -114,12 +114,12 @@ test('should update expiry of token', async () => {
|
||||
const time = new Date('2022-01-01');
|
||||
const newTime = new Date('2023-01-01');
|
||||
|
||||
const token = await apiTokenService.createApiToken(
|
||||
const token = await apiTokenService.createApiTokenWithProjects(
|
||||
{
|
||||
tokenName: 'default-client',
|
||||
type: ApiTokenType.CLIENT,
|
||||
expiresAt: time,
|
||||
project: '*',
|
||||
projects: ['*'],
|
||||
environment: DEFAULT_ENV,
|
||||
},
|
||||
TEST_AUDIT_USER,
|
||||
@ -133,7 +133,7 @@ test('should update expiry of token', async () => {
|
||||
});
|
||||
|
||||
test('should create client token with project list', async () => {
|
||||
const token = await apiTokenService.createApiToken({
|
||||
const token = await apiTokenService.createApiTokenWithProjects({
|
||||
tokenName: 'default-client',
|
||||
type: ApiTokenType.CLIENT,
|
||||
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 () => {
|
||||
const token = await apiTokenService.createApiToken({
|
||||
const token = await apiTokenService.createApiTokenWithProjects({
|
||||
tokenName: 'default-client',
|
||||
type: ApiTokenType.CLIENT,
|
||||
projects: ['*', 'default'],
|
||||
@ -159,21 +159,23 @@ test('should return user with multiple projects', async () => {
|
||||
const now = Date.now();
|
||||
const tomorrow = addDays(now, 1);
|
||||
|
||||
const { secret: secret1 } = await apiTokenService.createApiToken({
|
||||
tokenName: 'default-valid',
|
||||
type: ApiTokenType.CLIENT,
|
||||
expiresAt: tomorrow,
|
||||
projects: ['test-project', 'default'],
|
||||
environment: DEFAULT_ENV,
|
||||
});
|
||||
const { secret: secret1 } =
|
||||
await apiTokenService.createApiTokenWithProjects({
|
||||
tokenName: 'default-valid',
|
||||
type: ApiTokenType.CLIENT,
|
||||
expiresAt: tomorrow,
|
||||
projects: ['test-project', 'default'],
|
||||
environment: DEFAULT_ENV,
|
||||
});
|
||||
|
||||
const { secret: secret2 } = await apiTokenService.createApiToken({
|
||||
tokenName: 'default-also-valid',
|
||||
type: ApiTokenType.CLIENT,
|
||||
expiresAt: tomorrow,
|
||||
projects: ['test-project'],
|
||||
environment: DEFAULT_ENV,
|
||||
});
|
||||
const { secret: secret2 } =
|
||||
await apiTokenService.createApiTokenWithProjects({
|
||||
tokenName: 'default-also-valid',
|
||||
type: ApiTokenType.CLIENT,
|
||||
expiresAt: tomorrow,
|
||||
projects: ['test-project'],
|
||||
environment: DEFAULT_ENV,
|
||||
});
|
||||
|
||||
const multiProjectUser = await apiTokenService.getUserForToken(secret1);
|
||||
const singleProjectUser = await apiTokenService.getUserForToken(secret2);
|
||||
|
Loading…
Reference in New Issue
Block a user