diff --git a/docker-compose-enterprise.yml b/docker-compose-enterprise.yml index a958bbb8e0..b8847d66ce 100644 --- a/docker-compose-enterprise.yml +++ b/docker-compose-enterprise.yml @@ -28,7 +28,7 @@ services: LOG_LEVEL: "warn" INIT_FRONTEND_API_TOKENS: "default:development.unleash-insecure-frontend-api-token" # The default API token is insecure and should not be used in production. - INIT_CLIENT_API_TOKENS: "default:development.unleash-insecure-api-token" + INIT_BACKEND_API_TOKENS: "default:development.unleash-insecure-api-token" depends_on: db: condition: service_healthy diff --git a/docker-compose.yml b/docker-compose.yml index 708ca05222..57dad9856a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,7 +28,7 @@ services: LOG_LEVEL: "warn" INIT_FRONTEND_API_TOKENS: "default:development.unleash-insecure-frontend-api-token" # The default API token is insecure and should not be used in production. - INIT_CLIENT_API_TOKENS: "default:development.unleash-insecure-api-token" + INIT_BACKEND_API_TOKENS: "default:development.unleash-insecure-api-token" depends_on: db: condition: service_healthy diff --git a/src/lib/create-config.test.ts b/src/lib/create-config.test.ts index 8f4b2db7db..51b7521a80 100644 --- a/src/lib/create-config.test.ts +++ b/src/lib/create-config.test.ts @@ -1,6 +1,12 @@ import { createConfig, resolveIsOss } from './create-config.js'; import { ApiTokenType } from './types/model.js'; +beforeEach(() => { + delete process.env.INIT_BACKEND_API_TOKENS; + delete process.env.INIT_ADMIN_API_TOKENS; + delete process.env.INIT_CLIENT_API_TOKENS; + delete process.env.ENABLED_ENVIRONMENTS; +}); test('should create default config', async () => { const config = createConfig({ db: { @@ -65,7 +71,7 @@ test('should add initApiToken for client token from options', async () => { environment: 'development', projects: ['default'], secret: 'default:development.some-random-string', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, tokenName: 'admin', }; const config = createConfig({ @@ -92,7 +98,7 @@ test('should add initApiToken for client token from options', async () => { token.projects, ); expect(config.authentication.initApiTokens[0].type).toBe( - ApiTokenType.CLIENT, + ApiTokenType.BACKEND, ); }); @@ -123,8 +129,6 @@ test('should add initApiToken for admin token from env var', async () => { expect(config.authentication.initApiTokens[1].secret).toBe( '*:*.some-token2', ); - - delete process.env.INIT_ADMIN_API_TOKENS; }); test('should validate initApiToken for admin token from env var', async () => { @@ -133,23 +137,19 @@ test('should validate initApiToken for admin token from env var', async () => { expect(() => createConfig({})).toThrow( 'Admin token cannot be scoped to single project', ); - - delete process.env.INIT_ADMIN_API_TOKENS; }); test('should validate initApiToken for client token from env var', async () => { - process.env.INIT_CLIENT_API_TOKENS = '*:*:some-token1'; + process.env.INIT_BACKEND_API_TOKENS = '*:*:some-token1'; expect(() => createConfig({})).toThrow( 'Client token cannot be scoped to all environments', ); - - delete process.env.INIT_CLIENT_API_TOKENS; }); test('should merge initApiToken from options and env vars', async () => { process.env.INIT_ADMIN_API_TOKENS = '*:*.some-token1, *:*.some-token2'; - process.env.INIT_CLIENT_API_TOKENS = 'default:development.some-token1'; + process.env.INIT_BACKEND_API_TOKENS = 'default:development.some-token1'; const token = { environment: '*', projects: ['*'], @@ -174,43 +174,42 @@ test('should merge initApiToken from options and env vars', async () => { }); expect(config.authentication.initApiTokens).toHaveLength(4); - delete process.env.INIT_CLIENT_API_TOKENS; - delete process.env.INIT_ADMIN_API_TOKENS; }); -test('should add initApiToken for client token from env var', async () => { - process.env.INIT_CLIENT_API_TOKENS = - 'default:development.some-token1, default:development.some-token2'; +test.each([ApiTokenType.BACKEND, ApiTokenType.CLIENT])( + 'should add initApiToken for %s token from env var', + async (tokenType) => { + process.env[`INIT_${tokenType.toUpperCase()}_API_TOKENS`] = + 'default:development.some-token1, default:development.some-token2'; - const config = createConfig({ - db: { - host: 'localhost', - port: 4242, - user: 'unleash', - password: 'password', - database: 'unleash_db', - }, - server: { - port: 4242, - }, - }); + const config = createConfig({ + db: { + host: 'localhost', + port: 4242, + user: 'unleash', + password: 'password', + database: 'unleash_db', + }, + server: { + port: 4242, + }, + }); - expect(config.authentication.initApiTokens).toHaveLength(2); - expect(config.authentication.initApiTokens[0].environment).toBe( - 'development', - ); - expect(config.authentication.initApiTokens[0].projects).toMatchObject([ - 'default', - ]); - expect(config.authentication.initApiTokens[0].type).toBe( - ApiTokenType.CLIENT, - ); - expect(config.authentication.initApiTokens[0].secret).toBe( - 'default:development.some-token1', - ); - - delete process.env.INIT_CLIENT_API_TOKENS; -}); + expect(config.authentication.initApiTokens).toHaveLength(2); + expect(config.authentication.initApiTokens[0].environment).toBe( + 'development', + ); + expect(config.authentication.initApiTokens[0].projects).toMatchObject([ + 'default', + ]); + expect(config.authentication.initApiTokens[0].type).toBe( + ApiTokenType.BACKEND, + ); + expect(config.authentication.initApiTokens[0].secret).toBe( + 'default:development.some-token1', + ); + }, +); test('should handle cases where no env var specified for tokens', async () => { const token = { @@ -265,7 +264,6 @@ test('should load environment overrides from env var', async () => { expect(config.environmentEnableOverrides).toHaveLength(2); expect(config.environmentEnableOverrides).toContain('production'); - delete process.env.ENABLED_ENVIRONMENTS; }); test('should yield an empty list when no environment overrides are specified', async () => { @@ -510,7 +508,7 @@ test('Config with enterpriseVersion set and not pro environment should set isEnt test('create config should be idempotent in terms of tokens', async () => { // two admin tokens process.env.INIT_ADMIN_API_TOKENS = '*:*.some-token1, *:*.some-token2'; - process.env.INIT_CLIENT_API_TOKENS = 'default:development.some-token1'; + process.env.INIT_BACKEND_API_TOKENS = 'default:development.some-token1'; process.env.INIT_FRONTEND_API_TOKENS = 'frontend:development.some-token1'; const token = { environment: '*', @@ -538,9 +536,6 @@ test('create config should be idempotent in terms of tokens', async () => { createConfig(config).authentication.initApiTokens.length, ); expect(config.authentication.initApiTokens).toHaveLength(5); - delete process.env.INIT_ADMIN_API_TOKENS; - delete process.env.INIT_CLIENT_API_TOKENS; - delete process.env.INIT_FRONTEND_API_TOKENS; }); describe('isOSS', () => { diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index 2f775266b2..3512094ffd 100644 --- a/src/lib/create-config.ts +++ b/src/lib/create-config.ts @@ -436,8 +436,10 @@ const loadInitApiTokens = () => { ApiTokenType.ADMIN, ), ...loadTokensFromString( - process.env.INIT_CLIENT_API_TOKENS, - ApiTokenType.CLIENT, + process.env.INIT_BACKEND_API_TOKENS ?? + // INIT_CLIENT_API_TOKENS is deprecated in favor of INIT_BACKEND_API_TOKENS + process.env.INIT_CLIENT_API_TOKENS, + ApiTokenType.BACKEND, ), ...loadTokensFromString( process.env.INIT_FRONTEND_API_TOKENS, diff --git a/src/lib/db/api-token-store.ts b/src/lib/db/api-token-store.ts index 097bde7e37..f747119e15 100644 --- a/src/lib/db/api-token-store.ts +++ b/src/lib/db/api-token-store.ts @@ -284,7 +284,8 @@ export class ApiTokenStore implements IApiTokenStore { .andWhere('tokens.secret', 'LIKE', '%:%') // Exclude legacy tokens .andWhere((builder) => { builder - .where('tokens.type', ApiTokenType.CLIENT) + .where('tokens.type', ApiTokenType.BACKEND) + .orWhere('tokens.type', ApiTokenType.CLIENT) .orWhere('tokens.type', ApiTokenType.FRONTEND); }); diff --git a/src/lib/features/metrics/instance/metrics.test.ts b/src/lib/features/metrics/instance/metrics.test.ts index adfbdcb105..ad4dc6f53b 100644 --- a/src/lib/features/metrics/instance/metrics.test.ts +++ b/src/lib/features/metrics/instance/metrics.test.ts @@ -513,6 +513,13 @@ describe('bulk metrics', () => { environment: 'development', projects: ['*'], }); + const backendToken = + await authed.services.apiTokenService.createApiTokenWithProjects({ + tokenName: 'backend-bulk-metrics-test', + type: ApiTokenType.BACKEND, + environment: 'development', + projects: ['*'], + }); const frontendToken = await authed.services.apiTokenService.createApiTokenWithProjects({ tokenName: 'frontend-bulk-metrics-test', @@ -530,6 +537,11 @@ describe('bulk metrics', () => { .set('Authorization', frontendToken.secret) .send({ applications: [], metrics: [] }) .expect(403); + await authed.request + .post('/api/client/metrics/bulk') + .set('Authorization', backendToken.secret) + .send({ applications: [], metrics: [] }) + .expect(202); await authed.request .post('/api/client/metrics/bulk') .set('Authorization', clientToken.secret) diff --git a/src/lib/middleware/api-token-middleware.test.ts b/src/lib/middleware/api-token-middleware.test.ts index d169df9dec..1f9cccb168 100644 --- a/src/lib/middleware/api-token-middleware.test.ts +++ b/src/lib/middleware/api-token-middleware.test.ts @@ -90,7 +90,7 @@ test('should add user if known token', async () => { permissions: [CLIENT], project: ALL, environment: ALL, - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, secret: 'a', }); const apiTokenService = { @@ -114,46 +114,49 @@ test('should add user if known token', async () => { expect(req.user).toBe(apiUser); }); -test('should not add user if not /api/client', async () => { - expect.assertions(5); +test.each([ApiTokenType.CLIENT, ApiTokenType.BACKEND])( + 'should not add user if not /api/client with token type %s', + async (type) => { + expect.assertions(5); - const apiUser = new ApiUser({ - tokenName: 'default', - permissions: [CLIENT], - project: ALL, - environment: ALL, - type: ApiTokenType.CLIENT, - secret: 'a', - }); + const apiUser = new ApiUser({ + tokenName: 'default', + permissions: [CLIENT], + project: ALL, + environment: ALL, + type, + secret: 'a', + }); - const apiTokenService = { - getUserForToken: vi.fn().mockReturnValue(apiUser), - } as unknown as ApiTokenService; + const apiTokenService = { + getUserForToken: vi.fn().mockReturnValue(apiUser), + } as unknown as ApiTokenService; - const func = apiTokenMiddleware(config, { apiTokenService }); - const cb = vi.fn(); + const func = apiTokenMiddleware(config, { apiTokenService }); + const cb = vi.fn(); - const res = { - status: (code: unknown) => ({ - send: (data: unknown) => { - expect(code).toEqual(403); - expect(data).toEqual({ message: TOKEN_TYPE_ERROR_MESSAGE }); - }, - }), - }; + const res = { + status: (code: unknown) => ({ + send: (data: unknown) => { + expect(code).toEqual(403); + expect(data).toEqual({ message: TOKEN_TYPE_ERROR_MESSAGE }); + }, + }), + }; - const req = { - header: vi.fn().mockReturnValue('some-known-token'), - user: undefined, - path: '/api/admin', - }; + const req = { + header: vi.fn().mockReturnValue('some-known-token'), + user: undefined, + path: '/api/admin', + }; - await func(req, res, cb); + await func(req, res, cb); - expect(cb).not.toHaveBeenCalled(); - expect(req.header).toHaveBeenCalled(); - expect(req.user).toBeUndefined(); -}); + expect(cb).not.toHaveBeenCalled(); + expect(req.header).toHaveBeenCalled(); + expect(req.user).toBeUndefined(); + }, +); test('should not add user if disabled', async () => { const apiUser = new ApiUser({ @@ -161,7 +164,7 @@ test('should not add user if disabled', async () => { permissions: [CLIENT], project: ALL, environment: ALL, - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, secret: 'a', }); const apiTokenService = { @@ -252,7 +255,7 @@ test('should add user if client token and /edge/metrics', async () => { permissions: [CLIENT], project: ALL, environment: ALL, - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, secret: 'a', }); const apiTokenService = { diff --git a/src/lib/middleware/api-token-middleware.ts b/src/lib/middleware/api-token-middleware.ts index 1c17cdc4a3..f66039885f 100644 --- a/src/lib/middleware/api-token-middleware.ts +++ b/src/lib/middleware/api-token-middleware.ts @@ -57,11 +57,12 @@ const apiAccessMiddleware = ( const apiUser = apiToken ? await apiTokenService.getUserForToken(apiToken) : undefined; - const { CLIENT, FRONTEND } = ApiTokenType; + const { CLIENT, BACKEND, FRONTEND } = ApiTokenType; if (apiUser) { if ( - (apiUser.type === CLIENT && + ((apiUser.type === CLIENT || + apiUser.type === BACKEND) && !isClientApi(req) && !isEdgeMetricsApi(req)) || (apiUser.type === FRONTEND && !isProxyApi(req)) diff --git a/src/lib/middleware/rbac-middleware.test.ts b/src/lib/middleware/rbac-middleware.test.ts index ff0bfd4c84..420414a892 100644 --- a/src/lib/middleware/rbac-middleware.test.ts +++ b/src/lib/middleware/rbac-middleware.test.ts @@ -134,36 +134,39 @@ describe('ADMIN tokens should have user id -1337 when only passed through rbac-m }); }); -test('should not give api-user ADMIN permission', async () => { - const accessService = { - hasPermission: vi.fn(), - } as PermissionChecker; +test.each([ApiTokenType.BACKEND, ApiTokenType.CLIENT, ApiTokenType.FRONTEND])( + 'should not give api-user ADMIN permission to token %s', + async (tokenType) => { + const accessService = { + hasPermission: vi.fn(), + } as PermissionChecker; - const func = rbacMiddleware( - config, - { featureToggleStore, segmentStore }, - accessService, - ); + const func = rbacMiddleware( + config, + { featureToggleStore, segmentStore }, + accessService, + ); - const cb = vi.fn(); - const req: any = { - user: new ApiUser({ - tokenName: 'api', - permissions: [perms.CLIENT], - project: '*', - environment: '*', - type: ApiTokenType.CLIENT, - secret: 'a', - }), - }; + const cb = vi.fn(); + const req: any = { + user: new ApiUser({ + tokenName: `api_${tokenType}`, + permissions: [perms.CLIENT], + project: '*', + environment: '*', + type: tokenType, + secret: 'a', + }), + }; - func(req, undefined, cb); + func(req, undefined, cb); - const hasAccess = await req.checkRbac(perms.ADMIN); + const hasAccess = await req.checkRbac(perms.ADMIN); - expect(hasAccess).toBe(false); - expect(accessService.hasPermission).toHaveBeenCalledTimes(0); -}); + expect(hasAccess).toBe(false); + expect(accessService.hasPermission).toHaveBeenCalledTimes(0); + }, +); test('should not allow user to miss userId', async () => { vi.spyOn(global.console, 'error').mockImplementation(() => vi.fn()); diff --git a/src/lib/openapi/spec/api-token-schema.test.ts b/src/lib/openapi/spec/api-token-schema.test.ts index f4a5de0183..6f745f2ab1 100644 --- a/src/lib/openapi/spec/api-token-schema.test.ts +++ b/src/lib/openapi/spec/api-token-schema.test.ts @@ -14,13 +14,16 @@ const defaultData: ApiTokenSchema = { project: '', }; -test('apiTokenSchema', () => { - const data: ApiTokenSchema = { ...defaultData }; +test.each([ApiTokenType.CLIENT, ApiTokenType.BACKEND, ApiTokenType.FRONTEND])( + 'apiTokenSchema %s', + (tokenType) => { + const data: ApiTokenSchema = { ...defaultData, type: tokenType }; - expect( - validateSchema('#/components/schemas/apiTokenSchema', data), - ).toBeUndefined(); -}); + expect( + validateSchema('#/components/schemas/apiTokenSchema', data), + ).toBeUndefined(); + }, +); test('apiTokenSchema empty', () => { expect( diff --git a/src/lib/openapi/spec/create-api-token-schema.ts b/src/lib/openapi/spec/create-api-token-schema.ts index 777eab00ab..72123f383a 100644 --- a/src/lib/openapi/spec/create-api-token-schema.ts +++ b/src/lib/openapi/spec/create-api-token-schema.ts @@ -20,8 +20,8 @@ const clientFrontendSchema = { 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).`, + '^([Cc][Ll][Ii][Ee][Nn][Tt]|[Bb][Aa][Cc][Kk][Ee][Nn][Dd]|[Ff][Rr][Oo][Nn][Tt][Ee][Nn][Dd])$', + description: `A client or frontend token. Must be one of the strings "client" (deprecated), "backend" (preferred over "client") or "frontend" (not case sensitive).`, example: 'frontend', }, environment: { diff --git a/src/lib/openapi/spec/create-project-api-token-schema.ts b/src/lib/openapi/spec/create-project-api-token-schema.ts index 2cce98fe4e..80757dca53 100644 --- a/src/lib/openapi/spec/create-project-api-token-schema.ts +++ b/src/lib/openapi/spec/create-project-api-token-schema.ts @@ -10,8 +10,8 @@ export const createProjectApiTokenSchema = { 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).`, + '^([Cc][Ll][Ii][Ee][Nn][Tt]|[Bb][Aa][Cc][Kk][Ee][Nn][Dd]|[Ff][Rr][Oo][Nn][Tt][Ee][Nn][Dd])$', + description: `A client or frontend token. Must be one of the strings "client" (deprecated), "backend" (preferred over "client") or "frontend" (not case sensitive).`, example: 'frontend', }, environment: { diff --git a/src/lib/routes/admin-api/api-token.ts b/src/lib/routes/admin-api/api-token.ts index 31b6628023..c8e5f15cf5 100644 --- a/src/lib/routes/admin-api/api-token.ts +++ b/src/lib/routes/admin-api/api-token.ts @@ -57,6 +57,7 @@ export const tokenTypeToCreatePermission: (tokenType: ApiTokenType) => string = case ApiTokenType.ADMIN: return ADMIN; case ApiTokenType.CLIENT: + case ApiTokenType.BACKEND: return CREATE_CLIENT_API_TOKEN; case ApiTokenType.FRONTEND: return CREATE_FRONTEND_API_TOKEN; @@ -82,7 +83,7 @@ const permissionToTokenType: (permission: string) => ApiTokenType | undefined = UPDATE_CLIENT_API_TOKEN, ].includes(permission) ) { - return ApiTokenType.CLIENT; + return ApiTokenType.BACKEND; } else if (ADMIN === permission) { return ApiTokenType.ADMIN; } else { @@ -97,6 +98,7 @@ const tokenTypeToUpdatePermission: (tokenType: ApiTokenType) => string = ( case ApiTokenType.ADMIN: return ADMIN; case ApiTokenType.CLIENT: + case ApiTokenType.BACKEND: return UPDATE_CLIENT_API_TOKEN; case ApiTokenType.FRONTEND: return UPDATE_FRONTEND_API_TOKEN; @@ -110,6 +112,7 @@ const tokenTypeToDeletePermission: (tokenType: ApiTokenType) => string = ( case ApiTokenType.ADMIN: return ADMIN; case ApiTokenType.CLIENT: + case ApiTokenType.BACKEND: return DELETE_CLIENT_API_TOKEN; case ApiTokenType.FRONTEND: return DELETE_FRONTEND_API_TOKEN; diff --git a/src/lib/schema/api-token-schema.ts b/src/lib/schema/api-token-schema.ts index a8925ba1ba..e619e0f184 100644 --- a/src/lib/schema/api-token-schema.ts +++ b/src/lib/schema/api-token-schema.ts @@ -10,7 +10,11 @@ export const createApiToken = joi .string() .lowercase() .required() - .valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND), + .valid( + ApiTokenType.CLIENT, + ApiTokenType.BACKEND, + ApiTokenType.FRONTEND, + ), expiresAt: joi.date().optional(), projects: joi.array().min(1).optional().default([ALL]), environment: joi.string().optional().default('development'), diff --git a/src/lib/schema/create-project-api-token-schema.ts b/src/lib/schema/create-project-api-token-schema.ts index 3d78464ec4..9ed81b0578 100644 --- a/src/lib/schema/create-project-api-token-schema.ts +++ b/src/lib/schema/create-project-api-token-schema.ts @@ -10,10 +10,20 @@ export const createProjectApiToken = joi .string() .lowercase() .required() - .valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND), + .valid( + ApiTokenType.CLIENT, + ApiTokenType.BACKEND, + ApiTokenType.FRONTEND, + ), expiresAt: joi.date().optional(), environment: joi.when('type', { - is: joi.string().valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND), + is: joi + .string() + .valid( + ApiTokenType.CLIENT, + ApiTokenType.BACKEND, + ApiTokenType.FRONTEND, + ), then: joi.string().optional().default(DEFAULT_ENV), }), }) diff --git a/src/lib/services/api-token-service.limit.test.ts b/src/lib/services/api-token-service.limit.test.ts index 675417cfa0..bca81d40bc 100644 --- a/src/lib/services/api-token-service.limit.test.ts +++ b/src/lib/services/api-token-service.limit.test.ts @@ -45,7 +45,12 @@ test('Should allow you to create tokens up to and including the limit', async () } }); -test.each([ApiTokenType.ADMIN, ApiTokenType.CLIENT, ApiTokenType.FRONTEND])( +test.each([ + ApiTokenType.ADMIN, + ApiTokenType.CLIENT, + ApiTokenType.BACKEND, + ApiTokenType.FRONTEND, +])( "Should prevent you from creating %s tokens when you're already at the limit", async (tokenType) => { const limit = 1; @@ -58,7 +63,7 @@ test.each([ApiTokenType.ADMIN, ApiTokenType.CLIENT, ApiTokenType.FRONTEND])( await service.createApiTokenWithProjects( { - tokenName: 'token-1', + tokenName: `token-1-${tokenType}`, type: ApiTokenType.CLIENT, environment: 'production', projects: ['*'], diff --git a/src/lib/services/api-token-service.ts b/src/lib/services/api-token-service.ts index 0ade354860..4d7bca65ba 100644 --- a/src/lib/services/api-token-service.ts +++ b/src/lib/services/api-token-service.ts @@ -5,9 +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 { + ALL, resolveValidProjects, validateApiToken, - validateApiTokenEnvironment, } 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'; @@ -40,7 +40,10 @@ const resolveTokenPermissions = (tokenType: string) => { return [ADMIN]; } - if (tokenType === ApiTokenType.CLIENT) { + if ( + tokenType === ApiTokenType.BACKEND || + tokenType === ApiTokenType.CLIENT + ) { return [CLIENT]; } @@ -295,9 +298,7 @@ export class ApiTokenService { auditUser: IAuditUser, ): Promise { validateApiToken(newToken); - const environments = await this.environmentStore.getAll(); - validateApiTokenEnvironment(newToken, environments); - + await this.validateApiTokenEnvironment(newToken); await this.validateApiTokenLimit(); const secret = this.generateSecretKey(newToken); @@ -305,6 +306,19 @@ export class ApiTokenService { return this.insertNewApiToken(createNewToken, auditUser); } + private async validateApiTokenEnvironment({ + environment, + }: Pick): Promise { + if (environment === ALL) { + return; + } + + const exists = await this.environmentStore.exists(environment); + if (!exists) { + throw new BadDataError(`Environment=${environment} does not exist`); + } + } + private async validateApiTokenLimit() { const currentTokenCount = await this.store.count(); const limit = this.resourceLimits.apiTokens; diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 50884b5bda..afdbdf7dd0 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -197,9 +197,11 @@ export interface IFeatureDependency { export type IStrategyVariant = Omit; export enum ApiTokenType { + /** @deprecated: Use BACKEND instead */ CLIENT = 'client', ADMIN = 'admin', FRONTEND = 'frontend', + BACKEND = 'backend', } export interface IApiTokenCreate { diff --git a/src/lib/types/models/api-token.ts b/src/lib/types/models/api-token.ts index 02e920a625..154826f17d 100644 --- a/src/lib/types/models/api-token.ts +++ b/src/lib/types/models/api-token.ts @@ -1,5 +1,5 @@ import BadDataError from '../../error/bad-data-error.js'; -import type { IApiTokenCreate, IEnvironment } from '../model.js'; +import type { IApiTokenCreate } from '../model.js'; import { ApiTokenType } from '../model.js'; export const ALL = '*'; @@ -33,7 +33,10 @@ export const validateApiToken = ({ ); } - if (type === ApiTokenType.CLIENT && environment === ALL) { + if ( + (type === ApiTokenType.BACKEND || type === ApiTokenType.CLIENT) && + environment === ALL + ) { throw new BadDataError( 'Client token cannot be scoped to all environments', ); @@ -45,20 +48,3 @@ export const validateApiToken = ({ ); } }; - -export const validateApiTokenEnvironment = ( - { environment }: Pick, - environments: IEnvironment[], -): void => { - if (environment === ALL) { - return; - } - - const selectedEnvironment = environments.find( - (env) => env.name === environment, - ); - - if (!selectedEnvironment) { - throw new BadDataError(`Environment=${environment} does not exist`); - } -}; diff --git a/src/lib/types/permissions.ts b/src/lib/types/permissions.ts index 3b24b828fe..4747d55305 100644 --- a/src/lib/types/permissions.ts +++ b/src/lib/types/permissions.ts @@ -1,6 +1,6 @@ // Special export const ADMIN = 'ADMIN'; -export const CLIENT = 'CLIENT'; +export const CLIENT = 'CLIENT'; // TODO data migration needed to change to BACKEND export const FRONTEND = 'FRONTEND'; export const NONE = 'NONE'; 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 f81c6d2b0b..4403d5ace2 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 @@ -40,16 +40,6 @@ afterEach(async () => { await stores.apiTokenStore.deleteAll(); }); -const getLastEvent = async () => { - const events = await db.stores.eventStore.getEvents(); - return events.reduce((last, current) => { - if (current.id > last.id) { - return current; - } - return last; - }); -}; - test('editor users should only get client or frontend tokens', async () => { expect.assertions(3); @@ -77,7 +67,7 @@ test('editor users should only get client or frontend tokens', async () => { projects: [], tokenName: 'test', secret: '*:environment.1234', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, }); await stores.apiTokenStore.insert({ @@ -102,7 +92,7 @@ test('editor users should only get client or frontend tokens', async () => { .expect(200) .expect((res) => { expect(res.body.tokens.length).toBe(2); - expect(res.body.tokens[0].type).toBe(ApiTokenType.CLIENT); + expect(res.body.tokens[0].type).toBe(ApiTokenType.BACKEND); expect(res.body.tokens[1].type).toBe(ApiTokenType.FRONTEND); }); @@ -136,7 +126,7 @@ test('viewer users should not be allowed to fetch tokens', async () => { projects: [], tokenName: 'test', secret: '*:environment.1234', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, }); await stores.apiTokenStore.insert({ @@ -155,66 +145,70 @@ test('viewer users should not be allowed to fetch tokens', async () => { await destroy(); }); -test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', async () => { - expect.assertions(0); +test.each(['client', 'backend'])( + 'A role with only CREATE_PROJECT_API_TOKEN can create project %s token', + async (type) => { + expect.assertions(1); - const preHook = ( - app, - config, - { - userService, - accessService, - }: { userService: UserService; accessService: AccessService }, - ) => { - app.use('/api/admin/', async (req, res, next) => { - const role = (await accessService.getPredefinedRole( - RoleName.VIEWER, - ))!; - const user = await userService.createUser( - { - email: 'powerpuffgirls_viewer@example.com', - rootRole: role.id, - }, - SYSTEM_USER_AUDIT, - ); - const createClientApiTokenRole = await accessService.createRole( - { - name: 'project_client_token_creator', - description: 'Can create client tokens', - permissions: [{ name: CREATE_PROJECT_API_TOKEN }], - type: 'root-custom', - createdByUserId: SYSTEM_USER_ID, - }, - SYSTEM_USER_AUDIT, - ); - await accessService.addUserToRole( - user.id, - createClientApiTokenRole.id, - 'default', - ); - req.user = user; - next(); - }); - }; + const preHook = ( + app, + config, + { + userService, + accessService, + }: { userService: UserService; accessService: AccessService }, + ) => { + app.use('/api/admin/', async (req, res, next) => { + const role = (await accessService.getPredefinedRole( + RoleName.VIEWER, + ))!; + const user = await userService.createUser( + { + email: `powerpuffgirls_viewer_${type}@example.com`, + rootRole: role.id, + }, + SYSTEM_USER_AUDIT, + ); + const createClientApiTokenRole = await accessService.createRole( + { + name: `project_client_${type}_token_creator`, + description: `Can create ${type} tokens`, + permissions: [{ name: CREATE_PROJECT_API_TOKEN }], + type: 'root-custom', + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); + await accessService.addUserToRole( + user.id, + createClientApiTokenRole.id, + 'default', + ); + req.user = user; + next(); + }); + }; - const { request, destroy } = await setupAppWithCustomAuth( - stores, - preHook, - {}, - db.rawDatabase, - ); + const { request, destroy } = await setupAppWithCustomAuth( + stores, + preHook, + {}, + db.rawDatabase, + ); - await request - .post('/api/admin/projects/default/api-tokens') - .send({ - tokenName: 'client-token-maker', - type: 'client', - projects: ['default'], - }) - .set('Content-Type', 'application/json') - .expect(201); - await destroy(); -}); + const { body, status } = await request + .post('/api/admin/projects/default/api-tokens') + .send({ + tokenName: `${type}-token-maker`, + type, + projects: ['default'], + }) + .set('Content-Type', 'application/json'); + console.log(`Response: ${JSON.stringify(body)}`); + expect(status).toBe(201); + await destroy(); + }, +); describe('Fine grained API token permissions', () => { describe('A role with access to CREATE_CLIENT_API_TOKEN', () => { @@ -468,7 +462,7 @@ describe('Fine grained API token permissions', () => { projects: [], tokenName: 'client', secret: '*:environment.client_secret_1234', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, }); await stores.apiTokenStore.insert({ @@ -491,7 +485,7 @@ describe('Fine grained API token permissions', () => { .expect(200) .expect((res) => { expect(res.body.tokens).toHaveLength(1); - expect(res.body.tokens[0].type).toBe(ApiTokenType.CLIENT); + expect(res.body.tokens[0].type).toBe(ApiTokenType.BACKEND); }); await destroy(); }); @@ -527,7 +521,7 @@ describe('Fine grained API token permissions', () => { projects: [], tokenName: 'client', secret: '*:environment.client_secret_4321', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, }); await stores.apiTokenStore.insert({ @@ -585,7 +579,7 @@ describe('Fine grained API token permissions', () => { projects: [], tokenName: 'client', secret: '*:environment.client_secret_4321', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, }); await stores.apiTokenStore.insert({ environment: '', 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 11d1eba01f..6dc4de08b0 100644 --- a/src/test/e2e/api/admin/api-token.e2e.test.ts +++ b/src/test/e2e/api/admin/api-token.e2e.test.ts @@ -48,18 +48,18 @@ test('returns empty list of tokens', async () => { }); }); -test('creates new client token', async () => { - return app.request +test.each(['client', 'backend'])('creates new %s token', async (type) => { + await app.request .post('/api/admin/api-tokens') .send({ tokenName: 'default-client', - type: 'client', + type, }) .set('Content-Type', 'application/json') .expect(201) .expect((res) => { expect(res.body.tokenName).toBe('default-client'); - expect(res.body.type).toBe('client'); + expect(res.body.type).toBe(type); expect(res.body.createdAt).toBeTruthy(); expect(res.body.secret.length > 16).toBe(true); }); @@ -72,7 +72,7 @@ test('update client token with expiry', async () => { projects: ['*'], tokenName: 'test_token', secret: tokenSecret, - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, environment: 'development', }); @@ -94,40 +94,43 @@ test('update client token with expiry', async () => { }); }); -test('creates a lot of client tokens', async () => { - const requests: any[] = []; +test.each(['client', 'backend'])( + 'creates a lot of backend tokens from type %s', + async (type) => { + const requests: any[] = []; - for (let i = 0; i < 10; i++) { - requests.push( - app.request - .post('/api/admin/api-tokens') - .send({ - tokenName: 'default-client', - type: 'client', - }) - .set('Content-Type', 'application/json') - .expect(201), - ); - } - await Promise.all(requests); - expect.assertions(4); - await app.request - .get('/api/admin/api-tokens') - .expect('Content-Type', /json/) - .expect(200) - .expect((res) => { - expect(res.body.tokens.length).toBe(10); - expect(res.body.tokens[2].type).toBe('client'); - }); - await app.request - .get('/api/admin/api-tokens/default-client') - .expect('Content-Type', /json/) - .expect(200) - .expect((res) => { - expect(res.body.tokens.length).toBe(10); - expect(res.body.tokens[2].type).toBe('client'); - }); -}); + for (let i = 0; i < 10; i++) { + requests.push( + app.request + .post('/api/admin/api-tokens') + .send({ + tokenName: 'default-client', + type, + }) + .set('Content-Type', 'application/json') + .expect(201), + ); + } + await Promise.all(requests); + expect.assertions(4); + await app.request + .get('/api/admin/api-tokens') + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => { + expect(res.body.tokens.length).toBe(10); + expect(res.body.tokens[2].type).toBe(type); + }); + await app.request + .get('/api/admin/api-tokens/default-client') + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => { + expect(res.body.tokens.length).toBe(10); + expect(res.body.tokens[2].type).toBe(type); + }); + }, +); test('removes api token', async () => { const tokenSecret = '*:environment.random-secret'; @@ -137,7 +140,7 @@ test('removes api token', async () => { projects: ['*'], tokenName: 'testtoken', secret: tokenSecret, - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, }); await app.request @@ -154,41 +157,47 @@ test('removes api token', async () => { }); }); -test('creates new client token: project & environment defaults to "*"', async () => { - return app.request - .post('/api/admin/api-tokens') - .send({ - tokenName: 'default-client', - type: 'client', - }) - .set('Content-Type', 'application/json') - .expect(201) - .expect((res) => { - expect(res.body.type).toBe('client'); - expect(res.body.secret.length > 16).toBe(true); - expect(res.body.environment).toBe(DEFAULT_ENV); - expect(res.body.projects[0]).toBe(ALL); - }); -}); +test.each(['client', 'backend'])( + 'creates new %s token: project & environment defaults to "*"', + async (type) => { + await app.request + .post('/api/admin/api-tokens') + .send({ + tokenName: 'default-client', + type, + }) + .set('Content-Type', 'application/json') + .expect(201) + .expect((res) => { + expect(res.body.type).toBe(type); + expect(res.body.secret.length > 16).toBe(true); + expect(res.body.environment).toBe(DEFAULT_ENV); + expect(res.body.projects[0]).toBe(ALL); + }); + }, +); -test('creates new client token with project & environment set', async () => { - return app.request - .post('/api/admin/api-tokens') - .send({ - tokenName: 'default-client', - type: 'client', - projects: ['default'], - environment: DEFAULT_ENV, - }) - .set('Content-Type', 'application/json') - .expect(201) - .expect((res) => { - expect(res.body.type).toBe('client'); - expect(res.body.secret.length > 16).toBe(true); - expect(res.body.environment).toBe(DEFAULT_ENV); - expect(res.body.projects[0]).toBe('default'); - }); -}); +test.each(['client', 'backend'])( + 'creates new %s token with project & environment set', + async (type) => { + await app.request + .post('/api/admin/api-tokens') + .send({ + tokenName: 'default-client', + type, + projects: ['default'], + environment: DEFAULT_ENV, + }) + .set('Content-Type', 'application/json') + .expect(201) + .expect((res) => { + expect(res.body.type).toBe(type); + expect(res.body.secret.length > 16).toBe(true); + expect(res.body.environment).toBe(DEFAULT_ENV); + expect(res.body.projects[0]).toBe('default'); + }); + }, +); test('should prefix default token with "*:*."', async () => { return app.request @@ -324,3 +333,26 @@ test('Deleting non-existing token should yield 200', async () => { .delete('/api/admin/api-tokens/random-non-existing-token') .expect(200); }); + +test('having an existing client token in the db of type client still works', async () => { + await app.services.apiTokenService.createApiTokenWithProjects({ + tokenName: 'default-client', + type: ApiTokenType.CLIENT, + environment: DEFAULT_ENV, + projects: ['*'], + }); + + const { body } = await app.request + .get('/api/admin/api-tokens') + .expect('Content-Type', /json/) + .expect(200); + + const { tokens } = body; + expect(tokens.length).toBe(1); + expect(tokens[0]).toMatchObject({ + tokenName: 'default-client', + type: ApiTokenType.CLIENT, + environment: DEFAULT_ENV, + projects: ['*'], + }); +}); diff --git a/src/test/e2e/api/admin/applications.e2e.test.ts b/src/test/e2e/api/admin/applications.e2e.test.ts index 08789cbc39..f0f9f4f1b3 100644 --- a/src/test/e2e/api/admin/applications.e2e.test.ts +++ b/src/test/e2e/api/admin/applications.e2e.test.ts @@ -63,7 +63,7 @@ beforeAll(async () => { defaultToken = await app.services.apiTokenService.createApiTokenWithProjects({ - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, projects: ['default'], environment: DEFAULT_ENV, tokenName: 'tester', diff --git a/src/test/e2e/api/admin/instance-admin.e2e.test.ts b/src/test/e2e/api/admin/instance-admin.e2e.test.ts index 35496e8e88..8b169db94b 100644 --- a/src/test/e2e/api/admin/instance-admin.e2e.test.ts +++ b/src/test/e2e/api/admin/instance-admin.e2e.test.ts @@ -77,7 +77,7 @@ test('api tokens are serialized correctly', async () => { }); await app.services.apiTokenService.createApiTokenWithProjects({ tokenName: 'client', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, environment: DEFAULT_ENV, projects: ['*'], }); @@ -88,7 +88,7 @@ test('api tokens are serialized correctly', async () => { .expect(200); expect(body).toMatchObject({ - apiTokens: { client: 1, admin: 1, frontend: 1 }, + apiTokens: { backend: 1, admin: 1, frontend: 1 }, }); const { text: csv } = await app.request @@ -96,7 +96,7 @@ test('api tokens are serialized correctly', async () => { .expect('Content-Type', /text\/csv/) .expect(200); - expect(csv).toMatch(/{""client"":1,""admin"":1,""frontend"":1}/); + expect(csv).toMatch(/{""admin"":1,""frontend"":1,""backend"":1}/); }); test('should return instance statistics with correct number of projects', async () => { diff --git a/src/test/e2e/api/admin/metrics.e2e.test.ts b/src/test/e2e/api/admin/metrics.e2e.test.ts index b2c257c5d5..53e8b4e4a4 100644 --- a/src/test/e2e/api/admin/metrics.e2e.test.ts +++ b/src/test/e2e/api/admin/metrics.e2e.test.ts @@ -153,7 +153,7 @@ test('should save multiple projects from token', async () => { const multiProjectToken = await app.services.apiTokenService.createApiTokenWithProjects({ - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, projects: ['default', 'mainProject'], environment: DEFAULT_ENV, tokenName: 'tester', 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 ccddb65f1f..3a23142074 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 @@ -50,7 +50,7 @@ test('Returns list of tokens', async () => { await db.stores.apiTokenStore.insert({ tokenName: 'test', secret: tokenSecret, - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, environment: DEFAULT_ENV, projects: ['default'], }); @@ -87,21 +87,23 @@ test('fails to create new client token when given wrong project', async () => { .expect(404); }); -test('creates new client token', async () => { - return app.request - .post('/api/admin/projects/default/api-tokens') - .send({ - tokenName: 'default-client', - type: 'client', - projects: ['default'], - environment: DEFAULT_ENV, - }) - .set('Content-Type', 'application/json') - .expect(201) - .expect((res) => { - expect(res.body.tokenName).toBe('default-client'); - }); -}); +test.each(['client', 'frontend', 'backend'])( + 'creates new %s token', + async (type) => { + const { body, status } = await app.request + .post('/api/admin/projects/default/api-tokens') + .send({ + tokenName: `default-${type}`, + type, + projects: ['default'], + environment: DEFAULT_ENV, + }) + .set('Content-Type', 'application/json'); + console.log(body); + expect(status).toBe(201); + expect(body.tokenName).toBe(`default-${type}`); + }, +); test('Deletes existing tokens', async () => { const tokenSecret = 'random-secret'; @@ -109,7 +111,7 @@ test('Deletes existing tokens', async () => { await db.stores.apiTokenStore.insert({ tokenName: 'test', secret: tokenSecret, - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, environment: DEFAULT_ENV, projects: ['default'], }); @@ -142,7 +144,7 @@ test('Returns Bad Request when deleting tokens with more than one project', asyn await db.stores.apiTokenStore.insert({ tokenName: 'test', secret: tokenSecret, - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, environment: DEFAULT_ENV, projects: ['default', 'other'], }); diff --git a/src/test/e2e/api/auth/leading-slashes-are-stripped.e2e.test.ts b/src/test/e2e/api/auth/leading-slashes-are-stripped.e2e.test.ts index 768f23eca2..270d907704 100644 --- a/src/test/e2e/api/auth/leading-slashes-are-stripped.e2e.test.ts +++ b/src/test/e2e/api/auth/leading-slashes-are-stripped.e2e.test.ts @@ -67,7 +67,7 @@ test('Access with API token is granted', async () => { environment: DEFAULT_ENV, projects: ['default'], tokenName: 'test', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, }, ); await app.request diff --git a/src/test/e2e/api/client/feature.auth-none.e2e.test.ts b/src/test/e2e/api/client/feature.auth-none.e2e.test.ts index 776b34159c..70c438b9f9 100644 --- a/src/test/e2e/api/client/feature.auth-none.e2e.test.ts +++ b/src/test/e2e/api/client/feature.auth-none.e2e.test.ts @@ -61,7 +61,7 @@ beforeAll(async () => { const token = await app.services.apiTokenService.createApiTokenWithProjects( { tokenName: 'test', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, environment: DEFAULT_ENV, projects: ['default'], }, 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 9de19174a2..f96a1e6a7f 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 @@ -126,7 +126,7 @@ afterAll(async () => { test('returns feature flag with "default" config', async () => { const token = await apiTokenService.createApiTokenWithProjects({ - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, tokenName, environment: DEFAULT_ENV, projects: [project], @@ -148,7 +148,7 @@ test('returns feature flag with "default" config', async () => { test('returns feature flag with testing environment config', async () => { const token = await apiTokenService.createApiTokenWithProjects({ - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, tokenName: tokenName, environment, projects: [project], @@ -174,7 +174,7 @@ test('returns feature flag with testing environment config', async () => { test('returns feature flag for project2', async () => { const token = await apiTokenService.createApiTokenWithProjects({ - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, tokenName: tokenName, environment, projects: [project2], @@ -194,7 +194,7 @@ test('returns feature flag for project2', async () => { test('returns feature flag for all projects', async () => { const token = await apiTokenService.createApiTokenWithProjects({ - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, tokenName: tokenName, environment, projects: ['*'], 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 ba4c44fe3d..823922120e 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 @@ -134,7 +134,7 @@ afterAll(async () => { test('doesnt return feature flags if project deleted', async () => { const token = await apiTokenService.createApiTokenWithProjects({ - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, tokenName: deletionTokenName, environment, projects: [deletionProject], 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 af067d8997..415561fefd 100644 --- a/src/test/e2e/api/client/metrics.access.e2e.test.ts +++ b/src/test/e2e/api/client/metrics.access.e2e.test.ts @@ -32,7 +32,7 @@ test('should enrich metrics with environment from api-token', async () => { }); const token = await apiTokenService.createApiTokenWithProjects({ - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, tokenName: 'test', environment: 'some', projects: ['*'], diff --git a/src/test/e2e/api/client/metricsV2.e2e.test.ts b/src/test/e2e/api/client/metricsV2.e2e.test.ts index 98ca918b89..04d0404926 100644 --- a/src/test/e2e/api/client/metricsV2.e2e.test.ts +++ b/src/test/e2e/api/client/metricsV2.e2e.test.ts @@ -20,7 +20,7 @@ beforeAll(async () => { app = await setupAppWithAuth(db.stores, {}, db.rawDatabase); defaultToken = await app.services.apiTokenService.createApiTokenWithProjects({ - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, projects: ['default'], environment: DEFAULT_ENV, tokenName: 'tester', @@ -75,7 +75,7 @@ test('should pick up environment from token', async () => { await db.stores.environmentStore.create({ name: 'test', type: 'test' }); const token = await app.services.apiTokenService.createApiTokenWithProjects( { - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, projects: ['default'], environment, tokenName: 'tester', @@ -132,7 +132,7 @@ test('should set lastSeen for toggles with metrics both for toggle and toggle en const token = await app.services.apiTokenService.createApiTokenWithProjects( { - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, projects: ['default'], environment: DEFAULT_ENV, tokenName: 'tester', 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 cc7866fb44..75db223419 100644 --- a/src/test/e2e/services/api-token-service.e2e.test.ts +++ b/src/test/e2e/services/api-token-service.e2e.test.ts @@ -70,7 +70,7 @@ test('should have empty list of tokens', async () => { test('should create client token', async () => { const token = await apiTokenService.createApiTokenWithProjects({ tokenName: 'default-client', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, projects: ['*'], environment: DEFAULT_ENV, }); @@ -78,7 +78,7 @@ 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.type).toBe(ApiTokenType.BACKEND); expect(token.tokenName).toBe('default-client'); expect(allTokens[0].secret).toBe(token.secret); }); @@ -99,7 +99,7 @@ test('should set expiry of token', async () => { const time = new Date('2022-01-01'); await apiTokenService.createApiTokenWithProjects({ tokenName: 'default-client', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, expiresAt: time, projects: ['*'], environment: DEFAULT_ENV, @@ -117,7 +117,7 @@ test('should update expiry of token', async () => { const token = await apiTokenService.createApiTokenWithProjects( { tokenName: 'default-client', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, expiresAt: time, projects: ['*'], environment: DEFAULT_ENV, @@ -135,7 +135,7 @@ test('should update expiry of token', async () => { test('should create client token with project list', async () => { const token = await apiTokenService.createApiTokenWithProjects({ tokenName: 'default-client', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, projects: ['default', 'test-project'], environment: DEFAULT_ENV, }); @@ -147,7 +147,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.createApiTokenWithProjects({ tokenName: 'default-client', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, projects: ['*', 'default'], environment: DEFAULT_ENV, }); @@ -162,7 +162,7 @@ test('should return user with multiple projects', async () => { const { secret: secret1 } = await apiTokenService.createApiTokenWithProjects({ tokenName: 'default-valid', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, expiresAt: tomorrow, projects: ['test-project', 'default'], environment: DEFAULT_ENV, @@ -171,7 +171,7 @@ test('should return user with multiple projects', async () => { const { secret: secret2 } = await apiTokenService.createApiTokenWithProjects({ tokenName: 'default-also-valid', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, expiresAt: tomorrow, projects: ['test-project'], environment: DEFAULT_ENV, @@ -191,7 +191,7 @@ test('should not partially create token if projects are invalid', async () => { try { await apiTokenService.createApiTokenWithProjects({ tokenName: 'default-client', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, projects: ['non-existent-project'], environment: DEFAULT_ENV, }); diff --git a/src/test/e2e/services/edge-service.e2e.test.ts b/src/test/e2e/services/edge-service.e2e.test.ts index de8e378fb8..735b854c42 100644 --- a/src/test/e2e/services/edge-service.e2e.test.ts +++ b/src/test/e2e/services/edge-service.e2e.test.ts @@ -69,7 +69,7 @@ test('should only return valid tokens', async () => { const expiredToken = await stores.apiTokenStore.insert({ tokenName: 'expired', secret: '*:environment.expired-secret', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, expiresAt: yesterday, projects: ['*'], environment: DEFAULT_ENV, @@ -78,7 +78,7 @@ test('should only return valid tokens', async () => { const activeToken = await stores.apiTokenStore.insert({ tokenName: 'default-valid', secret: '*:environment.valid-secret', - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, expiresAt: tomorrow, projects: ['*'], environment: DEFAULT_ENV, diff --git a/src/test/e2e/stores/api-token-store.e2e.test.ts b/src/test/e2e/stores/api-token-store.e2e.test.ts index 57b6959aa7..c9101fec20 100644 --- a/src/test/e2e/stores/api-token-store.e2e.test.ts +++ b/src/test/e2e/stores/api-token-store.e2e.test.ts @@ -57,14 +57,14 @@ describe('count deprecated tokens', () => { await stores.apiTokenStore.insert({ secret: 'default:development.be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178', environment: DEFAULT_ENV, - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, projects: ['default'], tokenName: 'client-token', }); await stores.apiTokenStore.insert({ secret: '*:development.be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178', environment: DEFAULT_ENV, - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, projects: [], tokenName: 'client-wildcard-token', }); @@ -111,7 +111,7 @@ describe('count deprecated tokens', () => { await stores.apiTokenStore.insert({ secret: 'deleted-project:development.be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178', environment: DEFAULT_ENV, - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, projects: [], tokenName: 'admin-test-token', }); @@ -131,7 +131,7 @@ describe('count deprecated tokens', () => { await stores.apiTokenStore.insert({ secret: '*:*.be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178', environment: DEFAULT_ENV, - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, projects: [], tokenName: 'client-test-token', }); @@ -195,14 +195,14 @@ describe('count project tokens', () => { await store.insert({ secret: `default:default.${randomId()}`, environment: DEFAULT_ENV, - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, projects: ['default'], tokenName: 'token1', }); await store.insert({ secret: `*:*.${randomId()}`, environment: DEFAULT_ENV, - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, projects: ['*'], tokenName: 'token2', }); @@ -210,7 +210,7 @@ describe('count project tokens', () => { await store.insert({ secret: `${project.id}:default.${randomId()}`, environment: DEFAULT_ENV, - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, projects: [project.id], tokenName: 'token3', }); @@ -218,7 +218,7 @@ describe('count project tokens', () => { await store.insert({ secret: `[]:default.${randomId()}`, environment: DEFAULT_ENV, - type: ApiTokenType.CLIENT, + type: ApiTokenType.BACKEND, projects: [project.id, 'default'], tokenName: 'token4', }); diff --git a/website/docs/using-unleash/deploy/configuring-unleash.mdx b/website/docs/using-unleash/deploy/configuring-unleash.mdx index 58dd636dc2..2e78712892 100644 --- a/website/docs/using-unleash/deploy/configuring-unleash.mdx +++ b/website/docs/using-unleash/deploy/configuring-unleash.mdx @@ -185,7 +185,8 @@ If emails fail to send or contain errors: | :--------------------------------- | :-------------- | :--------------------------------------------------------------------------------------------------------- | | `UNLEASH_DEFAULT_ADMIN_USERNAME` | `admin` | Sets the username for the initial admin user created on first startup. | | `UNLEASH_DEFAULT_ADMIN_PASSWORD` | `unleash4all` | Sets the password for the initial admin user. **Change this for any non-local setup.** | -| `INIT_CLIENT_API_TOKENS` | N/A | Comma-separated list of [Client tokens](/reference/api-tokens-and-client-keys#backend-tokens) to create on first startup (if no tokens exist in the database). | +| `INIT_CLIENT_API_TOKENS` | N/A | Deprecated, use `INIT_BACKEND_API_TOKENS` instead. | +| `INIT_BACKEND_API_TOKENS` | N/A | Comma-separated list of [Backend tokens](/reference/api-tokens-and-client-keys#backend-tokens) to create on first startup (if no tokens exist in the database). | | `INIT_FRONTEND_API_TOKENS` | N/A | Comma-separated list of [Frontend tokens](/reference/api-tokens-and-client-keys#frontend-tokens) to create on first startup (if no tokens exist in the database). | ### Server behavior