diff --git a/src/lib/create-config.test.ts b/src/lib/create-config.test.ts index 5bba36eced..8f4b2db7db 100644 --- a/src/lib/create-config.test.ts +++ b/src/lib/create-config.test.ts @@ -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', diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index a82a763cf6..45b9b588cd 100644 --- a/src/lib/create-config.ts +++ b/src/lib/create-config.ts @@ -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; diff --git a/src/lib/db/api-token-store.ts b/src/lib/db/api-token-store.ts index e15265d7ad..097bde7e37 100644 --- a/src/lib/db/api-token-store.ts +++ b/src/lib/db/api-token-store.ts @@ -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, diff --git a/src/lib/features/project/project-service.e2e.test.ts b/src/lib/features/project/project-service.e2e.test.ts index 068300b77d..58a2a0462e 100644 --- a/src/lib/features/project/project-service.e2e.test.ts +++ b/src/lib/features/project/project-service.e2e.test.ts @@ -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, diff --git a/src/lib/openapi/spec/api-token-schema.test.ts b/src/lib/openapi/spec/api-token-schema.test.ts index d6b20b1cc2..f4a5de0183 100644 --- a/src/lib/openapi/spec/api-token-schema.test.ts +++ b/src/lib/openapi/spec/api-token-schema.test.ts @@ -4,7 +4,6 @@ import type { ApiTokenSchema } from './api-token-schema.js'; const defaultData: ApiTokenSchema = { secret: '', - username: '', tokenName: '', type: ApiTokenType.CLIENT, environment: '', diff --git a/src/lib/openapi/spec/api-token-schema.ts b/src/lib/openapi/spec/api-token-schema.ts index 098e181d13..3172a92f0b 100644 --- a/src/lib/openapi/spec/api-token-schema.ts +++ b/src/lib/openapi/spec/api-token-schema.ts @@ -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', diff --git a/src/lib/openapi/spec/create-api-token-schema.ts b/src/lib/openapi/spec/create-api-token-schema.ts index e819c5cd64..777eab00ab 100644 --- a/src/lib/openapi/spec/create-api-token-schema.ts +++ b/src/lib/openapi/spec/create-api-token-schema.ts @@ -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; diff --git a/src/lib/openapi/spec/create-project-api-token-schema.ts b/src/lib/openapi/spec/create-project-api-token-schema.ts new file mode 100644 index 0000000000..2cce98fe4e --- /dev/null +++ b/src/lib/openapi/spec/create-project-api-token-schema.ts @@ -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 +>; diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts index f48bcddf5c..04771b2f0e 100644 --- a/src/lib/openapi/spec/index.ts +++ b/src/lib/openapi/spec/index.ts @@ -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'; diff --git a/src/lib/routes/admin-api/api-token.test.ts b/src/lib/routes/admin-api/api-token.test.ts deleted file mode 100644 index 2f3cf0af2c..0000000000 --- a/src/lib/routes/admin-api/api-token.test.ts +++ /dev/null @@ -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(); - }); - }); -}); diff --git a/src/lib/routes/admin-api/api-token.ts b/src/lib/routes/admin-api/api-token.ts index 40b971a0f0..31b6628023 100644 --- a/src/lib/routes/admin-api/api-token.ts +++ b/src/lib/routes/admin-api/api-token.ts @@ -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, res: Response, ): Promise { 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, ); diff --git a/src/lib/routes/admin-api/project/api-token.ts b/src/lib/routes/admin-api/project/api-token.ts index 9ec163f87e..38baecbbf1 100644 --- a/src/lib/routes/admin-api/project/api-token.ts +++ b/src/lib/routes/admin-api/project/api-token.ts @@ -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, ): Promise { - 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( diff --git a/src/lib/schema/api-token-schema.test.ts b/src/lib/schema/api-token-schema.test.ts index e49101badc..c05537374a 100644 --- a/src/lib/schema/api-token-schema.test.ts +++ b/src/lib/schema/api-token-schema.test.ts @@ -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'); }); diff --git a/src/lib/schema/api-token-schema.ts b/src/lib/schema/api-token-schema.ts index 1142b8fae9..00078d3888 100644 --- a/src/lib/schema/api-token-schema.ts +++ b/src/lib/schema/api-token-schema.ts @@ -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 }); diff --git a/src/lib/schema/create-project-api-token-schema.ts b/src/lib/schema/create-project-api-token-schema.ts new file mode 100644 index 0000000000..3d78464ec4 --- /dev/null +++ b/src/lib/schema/create-project-api-token-schema.ts @@ -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 }); diff --git a/src/lib/services/api-token-service.test.ts b/src/lib/services/api-token-service.test.ts index cd666cf0b9..bba25794e1 100644 --- a/src/lib/services/api-token-service.test.ts +++ b/src/lib/services/api-token-service.test.ts @@ -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', diff --git a/src/lib/services/api-token-service.ts b/src/lib/services/api-token-service.ts index aec99c7d5a..ca1752eca1 100644 --- a/src/lib/services/api-token-service.ts +++ b/src/lib/services/api-token-service.ts @@ -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, - auditUser: IAuditUser = SYSTEM_USER_AUDIT, - ): Promise { - 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, auditUser: IAuditUser = SYSTEM_USER_AUDIT, ): Promise { - return this.internalCreateApiTokenWithProjects(newToken, auditUser); + return this.internalCreateApiTokenWithProjects( + { + ...newToken, + projects: resolveValidProjects(newToken.projects), + }, + auditUser, + ); } private async internalCreateApiTokenWithProjects( diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 40c1597bbf..999dacb506 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -210,10 +210,6 @@ export interface IApiTokenCreate { environment: string; projects: string[]; expiresAt?: Date; - /** - * @deprecated Use tokenName instead - */ - username?: string; } export interface IApiToken extends Omit { diff --git a/src/lib/types/models/api-token.ts b/src/lib/types/models/api-token.ts index 950edc141e..02e920a625 100644 --- a/src/lib/types/models/api-token.ts +++ b/src/lib/types/models/api-token.ts @@ -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, -): Omit => { - 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 = ({ diff --git a/src/lib/types/option.ts b/src/lib/types/option.ts index d8f1553912..fedb1eca03 100644 --- a/src/lib/types/option.ts +++ b/src/lib/types/option.ts @@ -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 { diff --git a/src/server-dev.ts b/src/server-dev.ts index 1ab2b704ab..4f9c6d9a01 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -63,7 +63,7 @@ process.nextTick(async () => { initApiTokens: [ { environment: '*', - project: '*', + projects: ['*'], secret: '*:*.964a287e1b728cb5f4f3e0120df92cb5', type: ApiTokenType.ADMIN, tokenName: 'some-user', diff --git a/src/test/e2e/api/admin/api-token.auth.e2e.test.ts b/src/test/e2e/api/admin/api-token.auth.e2e.test.ts index 6266aecc1c..f81c6d2b0b 100644 --- a/src/test/e2e/api/admin/api-token.auth.e2e.test.ts +++ b/src/test/e2e/api/admin/api-token.auth.e2e.test.ts @@ -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, - ) => { - 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, }); diff --git a/src/test/e2e/api/admin/api-token.e2e.test.ts b/src/test/e2e/api/admin/api-token.e2e.test.ts index d7a2636b7b..7908ddbe0a 100644 --- a/src/test/e2e/api/admin/api-token.e2e.test.ts +++ b/src/test/e2e/api/admin/api-token.e2e.test.ts @@ -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') diff --git a/src/test/e2e/api/admin/project/project.api.tokens.e2e.test.ts b/src/test/e2e/api/admin/project/project.api.tokens.e2e.test.ts index 39c16d9fda..59051fdd7a 100644 --- a/src/test/e2e/api/admin/project/project.api.tokens.e2e.test.ts +++ b/src/test/e2e/api/admin/project/project.api.tokens.e2e.test.ts @@ -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'); }); }); diff --git a/src/test/e2e/api/client/feature.token.access.e2e.test.ts b/src/test/e2e/api/client/feature.token.access.e2e.test.ts index 09e6e64727..9de19174a2 100644 --- a/src/test/e2e/api/client/feature.token.access.e2e.test.ts +++ b/src/test/e2e/api/client/feature.token.access.e2e.test.ts @@ -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') diff --git a/src/test/e2e/api/client/feature.token.deleted.project.e2e.test.ts b/src/test/e2e/api/client/feature.token.deleted.project.e2e.test.ts index e90a37b681..ba4c44fe3d 100644 --- a/src/test/e2e/api/client/feature.token.deleted.project.e2e.test.ts +++ b/src/test/e2e/api/client/feature.token.deleted.project.e2e.test.ts @@ -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( diff --git a/src/test/e2e/api/client/metrics.access.e2e.test.ts b/src/test/e2e/api/client/metrics.access.e2e.test.ts index 7cd19f16f8..af067d8997 100644 --- a/src/test/e2e/api/client/metrics.access.e2e.test.ts +++ b/src/test/e2e/api/client/metrics.access.e2e.test.ts @@ -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]; diff --git a/src/test/e2e/api/client/metricsV2.e2e.test.ts b/src/test/e2e/api/client/metricsV2.e2e.test.ts index 81b0111e55..9618c3d716 100644 --- a/src/test/e2e/api/client/metricsV2.e2e.test.ts +++ b/src/test/e2e/api/client/metricsV2.e2e.test.ts @@ -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>() .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') diff --git a/src/test/e2e/services/api-token-service.e2e.test.ts b/src/test/e2e/services/api-token-service.e2e.test.ts index 46c21917ad..cc7866fb44 100644 --- a/src/test/e2e/services/api-token-service.e2e.test.ts +++ b/src/test/e2e/services/api-token-service.e2e.test.ts @@ -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);