1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-19 17:52:45 +02:00

feat: incorporate backend as a valid api token type replacing client (#10500)

This PR deprecates `CLIENT` api token type in favor of `BACKEND` but
both will continue working.

Also replaces:
- `INIT_CLIENT_API_TOKENS` with `INIT_BACKEND_API_TOKENS`. The former is
kept for backward compatibility.
This commit is contained in:
Gastón Fournier 2025-08-21 05:43:54 -07:00 committed by GitHub
parent 02ee94c38f
commit 92480554dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 431 additions and 358 deletions

View File

@ -28,7 +28,7 @@ services:
LOG_LEVEL: "warn" LOG_LEVEL: "warn"
INIT_FRONTEND_API_TOKENS: "default:development.unleash-insecure-frontend-api-token" INIT_FRONTEND_API_TOKENS: "default:development.unleash-insecure-frontend-api-token"
# The default API token is insecure and should not be used in production. # 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: depends_on:
db: db:
condition: service_healthy condition: service_healthy

View File

@ -28,7 +28,7 @@ services:
LOG_LEVEL: "warn" LOG_LEVEL: "warn"
INIT_FRONTEND_API_TOKENS: "default:development.unleash-insecure-frontend-api-token" INIT_FRONTEND_API_TOKENS: "default:development.unleash-insecure-frontend-api-token"
# The default API token is insecure and should not be used in production. # 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: depends_on:
db: db:
condition: service_healthy condition: service_healthy

View File

@ -1,6 +1,12 @@
import { createConfig, resolveIsOss } from './create-config.js'; import { createConfig, resolveIsOss } from './create-config.js';
import { ApiTokenType } from './types/model.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 () => { test('should create default config', async () => {
const config = createConfig({ const config = createConfig({
db: { db: {
@ -65,7 +71,7 @@ test('should add initApiToken for client token from options', async () => {
environment: 'development', environment: 'development',
projects: ['default'], projects: ['default'],
secret: 'default:development.some-random-string', secret: 'default:development.some-random-string',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
tokenName: 'admin', tokenName: 'admin',
}; };
const config = createConfig({ const config = createConfig({
@ -92,7 +98,7 @@ test('should add initApiToken for client token from options', async () => {
token.projects, token.projects,
); );
expect(config.authentication.initApiTokens[0].type).toBe( 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( expect(config.authentication.initApiTokens[1].secret).toBe(
'*:*.some-token2', '*:*.some-token2',
); );
delete process.env.INIT_ADMIN_API_TOKENS;
}); });
test('should validate initApiToken for admin token from env var', async () => { 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( expect(() => createConfig({})).toThrow(
'Admin token cannot be scoped to single project', '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 () => { 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( expect(() => createConfig({})).toThrow(
'Client token cannot be scoped to all environments', '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 () => { test('should merge initApiToken from options and env vars', async () => {
process.env.INIT_ADMIN_API_TOKENS = '*:*.some-token1, *:*.some-token2'; 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 = { const token = {
environment: '*', environment: '*',
projects: ['*'], projects: ['*'],
@ -174,12 +174,12 @@ test('should merge initApiToken from options and env vars', async () => {
}); });
expect(config.authentication.initApiTokens).toHaveLength(4); 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 () => { test.each([ApiTokenType.BACKEND, ApiTokenType.CLIENT])(
process.env.INIT_CLIENT_API_TOKENS = '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'; 'default:development.some-token1, default:development.some-token2';
const config = createConfig({ const config = createConfig({
@ -203,14 +203,13 @@ test('should add initApiToken for client token from env var', async () => {
'default', 'default',
]); ]);
expect(config.authentication.initApiTokens[0].type).toBe( expect(config.authentication.initApiTokens[0].type).toBe(
ApiTokenType.CLIENT, ApiTokenType.BACKEND,
); );
expect(config.authentication.initApiTokens[0].secret).toBe( expect(config.authentication.initApiTokens[0].secret).toBe(
'default:development.some-token1', 'default:development.some-token1',
); );
},
delete process.env.INIT_CLIENT_API_TOKENS; );
});
test('should handle cases where no env var specified for tokens', async () => { test('should handle cases where no env var specified for tokens', async () => {
const token = { const token = {
@ -265,7 +264,6 @@ test('should load environment overrides from env var', async () => {
expect(config.environmentEnableOverrides).toHaveLength(2); expect(config.environmentEnableOverrides).toHaveLength(2);
expect(config.environmentEnableOverrides).toContain('production'); expect(config.environmentEnableOverrides).toContain('production');
delete process.env.ENABLED_ENVIRONMENTS;
}); });
test('should yield an empty list when no environment overrides are specified', async () => { 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 () => { test('create config should be idempotent in terms of tokens', async () => {
// two admin tokens // two admin tokens
process.env.INIT_ADMIN_API_TOKENS = '*:*.some-token1, *:*.some-token2'; 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'; process.env.INIT_FRONTEND_API_TOKENS = 'frontend:development.some-token1';
const token = { const token = {
environment: '*', environment: '*',
@ -538,9 +536,6 @@ test('create config should be idempotent in terms of tokens', async () => {
createConfig(config).authentication.initApiTokens.length, createConfig(config).authentication.initApiTokens.length,
); );
expect(config.authentication.initApiTokens).toHaveLength(5); 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', () => { describe('isOSS', () => {

View File

@ -436,8 +436,10 @@ const loadInitApiTokens = () => {
ApiTokenType.ADMIN, ApiTokenType.ADMIN,
), ),
...loadTokensFromString( ...loadTokensFromString(
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, process.env.INIT_CLIENT_API_TOKENS,
ApiTokenType.CLIENT, ApiTokenType.BACKEND,
), ),
...loadTokensFromString( ...loadTokensFromString(
process.env.INIT_FRONTEND_API_TOKENS, process.env.INIT_FRONTEND_API_TOKENS,

View File

@ -284,7 +284,8 @@ export class ApiTokenStore implements IApiTokenStore {
.andWhere('tokens.secret', 'LIKE', '%:%') // Exclude legacy tokens .andWhere('tokens.secret', 'LIKE', '%:%') // Exclude legacy tokens
.andWhere((builder) => { .andWhere((builder) => {
builder builder
.where('tokens.type', ApiTokenType.CLIENT) .where('tokens.type', ApiTokenType.BACKEND)
.orWhere('tokens.type', ApiTokenType.CLIENT)
.orWhere('tokens.type', ApiTokenType.FRONTEND); .orWhere('tokens.type', ApiTokenType.FRONTEND);
}); });

View File

@ -513,6 +513,13 @@ describe('bulk metrics', () => {
environment: 'development', environment: 'development',
projects: ['*'], projects: ['*'],
}); });
const backendToken =
await authed.services.apiTokenService.createApiTokenWithProjects({
tokenName: 'backend-bulk-metrics-test',
type: ApiTokenType.BACKEND,
environment: 'development',
projects: ['*'],
});
const frontendToken = const frontendToken =
await authed.services.apiTokenService.createApiTokenWithProjects({ await authed.services.apiTokenService.createApiTokenWithProjects({
tokenName: 'frontend-bulk-metrics-test', tokenName: 'frontend-bulk-metrics-test',
@ -530,6 +537,11 @@ describe('bulk metrics', () => {
.set('Authorization', frontendToken.secret) .set('Authorization', frontendToken.secret)
.send({ applications: [], metrics: [] }) .send({ applications: [], metrics: [] })
.expect(403); .expect(403);
await authed.request
.post('/api/client/metrics/bulk')
.set('Authorization', backendToken.secret)
.send({ applications: [], metrics: [] })
.expect(202);
await authed.request await authed.request
.post('/api/client/metrics/bulk') .post('/api/client/metrics/bulk')
.set('Authorization', clientToken.secret) .set('Authorization', clientToken.secret)

View File

@ -90,7 +90,7 @@ test('should add user if known token', async () => {
permissions: [CLIENT], permissions: [CLIENT],
project: ALL, project: ALL,
environment: ALL, environment: ALL,
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
secret: 'a', secret: 'a',
}); });
const apiTokenService = { const apiTokenService = {
@ -114,7 +114,9 @@ test('should add user if known token', async () => {
expect(req.user).toBe(apiUser); expect(req.user).toBe(apiUser);
}); });
test('should not add user if not /api/client', async () => { test.each([ApiTokenType.CLIENT, ApiTokenType.BACKEND])(
'should not add user if not /api/client with token type %s',
async (type) => {
expect.assertions(5); expect.assertions(5);
const apiUser = new ApiUser({ const apiUser = new ApiUser({
@ -122,7 +124,7 @@ test('should not add user if not /api/client', async () => {
permissions: [CLIENT], permissions: [CLIENT],
project: ALL, project: ALL,
environment: ALL, environment: ALL,
type: ApiTokenType.CLIENT, type,
secret: 'a', secret: 'a',
}); });
@ -153,7 +155,8 @@ test('should not add user if not /api/client', async () => {
expect(cb).not.toHaveBeenCalled(); expect(cb).not.toHaveBeenCalled();
expect(req.header).toHaveBeenCalled(); expect(req.header).toHaveBeenCalled();
expect(req.user).toBeUndefined(); expect(req.user).toBeUndefined();
}); },
);
test('should not add user if disabled', async () => { test('should not add user if disabled', async () => {
const apiUser = new ApiUser({ const apiUser = new ApiUser({
@ -161,7 +164,7 @@ test('should not add user if disabled', async () => {
permissions: [CLIENT], permissions: [CLIENT],
project: ALL, project: ALL,
environment: ALL, environment: ALL,
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
secret: 'a', secret: 'a',
}); });
const apiTokenService = { const apiTokenService = {
@ -252,7 +255,7 @@ test('should add user if client token and /edge/metrics', async () => {
permissions: [CLIENT], permissions: [CLIENT],
project: ALL, project: ALL,
environment: ALL, environment: ALL,
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
secret: 'a', secret: 'a',
}); });
const apiTokenService = { const apiTokenService = {

View File

@ -57,11 +57,12 @@ const apiAccessMiddleware = (
const apiUser = apiToken const apiUser = apiToken
? await apiTokenService.getUserForToken(apiToken) ? await apiTokenService.getUserForToken(apiToken)
: undefined; : undefined;
const { CLIENT, FRONTEND } = ApiTokenType; const { CLIENT, BACKEND, FRONTEND } = ApiTokenType;
if (apiUser) { if (apiUser) {
if ( if (
(apiUser.type === CLIENT && ((apiUser.type === CLIENT ||
apiUser.type === BACKEND) &&
!isClientApi(req) && !isClientApi(req) &&
!isEdgeMetricsApi(req)) || !isEdgeMetricsApi(req)) ||
(apiUser.type === FRONTEND && !isProxyApi(req)) (apiUser.type === FRONTEND && !isProxyApi(req))

View File

@ -134,7 +134,9 @@ describe('ADMIN tokens should have user id -1337 when only passed through rbac-m
}); });
}); });
test('should not give api-user ADMIN permission', async () => { test.each([ApiTokenType.BACKEND, ApiTokenType.CLIENT, ApiTokenType.FRONTEND])(
'should not give api-user ADMIN permission to token %s',
async (tokenType) => {
const accessService = { const accessService = {
hasPermission: vi.fn(), hasPermission: vi.fn(),
} as PermissionChecker; } as PermissionChecker;
@ -148,11 +150,11 @@ test('should not give api-user ADMIN permission', async () => {
const cb = vi.fn(); const cb = vi.fn();
const req: any = { const req: any = {
user: new ApiUser({ user: new ApiUser({
tokenName: 'api', tokenName: `api_${tokenType}`,
permissions: [perms.CLIENT], permissions: [perms.CLIENT],
project: '*', project: '*',
environment: '*', environment: '*',
type: ApiTokenType.CLIENT, type: tokenType,
secret: 'a', secret: 'a',
}), }),
}; };
@ -163,7 +165,8 @@ test('should not give api-user ADMIN permission', async () => {
expect(hasAccess).toBe(false); expect(hasAccess).toBe(false);
expect(accessService.hasPermission).toHaveBeenCalledTimes(0); expect(accessService.hasPermission).toHaveBeenCalledTimes(0);
}); },
);
test('should not allow user to miss userId', async () => { test('should not allow user to miss userId', async () => {
vi.spyOn(global.console, 'error').mockImplementation(() => vi.fn()); vi.spyOn(global.console, 'error').mockImplementation(() => vi.fn());

View File

@ -14,13 +14,16 @@ const defaultData: ApiTokenSchema = {
project: '', project: '',
}; };
test('apiTokenSchema', () => { test.each([ApiTokenType.CLIENT, ApiTokenType.BACKEND, ApiTokenType.FRONTEND])(
const data: ApiTokenSchema = { ...defaultData }; 'apiTokenSchema %s',
(tokenType) => {
const data: ApiTokenSchema = { ...defaultData, type: tokenType };
expect( expect(
validateSchema('#/components/schemas/apiTokenSchema', data), validateSchema('#/components/schemas/apiTokenSchema', data),
).toBeUndefined(); ).toBeUndefined();
}); },
);
test('apiTokenSchema empty', () => { test('apiTokenSchema empty', () => {
expect( expect(

View File

@ -20,8 +20,8 @@ const clientFrontendSchema = {
type: { type: {
type: 'string', type: 'string',
pattern: pattern:
'^([Cc][Ll][Ii][Ee][Nn][Tt]|[Ff][Rr][Oo][Nn][Tt][Ee][Nn][Dd])$', '^([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" or "frontend" (not case sensitive).`, 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', example: 'frontend',
}, },
environment: { environment: {

View File

@ -10,8 +10,8 @@ export const createProjectApiTokenSchema = {
type: { type: {
type: 'string', type: 'string',
pattern: pattern:
'^([Cc][Ll][Ii][Ee][Nn][Tt]|[Ff][Rr][Oo][Nn][Tt][Ee][Nn][Dd])$', '^([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" or "frontend" (not case sensitive).`, 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', example: 'frontend',
}, },
environment: { environment: {

View File

@ -57,6 +57,7 @@ export const tokenTypeToCreatePermission: (tokenType: ApiTokenType) => string =
case ApiTokenType.ADMIN: case ApiTokenType.ADMIN:
return ADMIN; return ADMIN;
case ApiTokenType.CLIENT: case ApiTokenType.CLIENT:
case ApiTokenType.BACKEND:
return CREATE_CLIENT_API_TOKEN; return CREATE_CLIENT_API_TOKEN;
case ApiTokenType.FRONTEND: case ApiTokenType.FRONTEND:
return CREATE_FRONTEND_API_TOKEN; return CREATE_FRONTEND_API_TOKEN;
@ -82,7 +83,7 @@ const permissionToTokenType: (permission: string) => ApiTokenType | undefined =
UPDATE_CLIENT_API_TOKEN, UPDATE_CLIENT_API_TOKEN,
].includes(permission) ].includes(permission)
) { ) {
return ApiTokenType.CLIENT; return ApiTokenType.BACKEND;
} else if (ADMIN === permission) { } else if (ADMIN === permission) {
return ApiTokenType.ADMIN; return ApiTokenType.ADMIN;
} else { } else {
@ -97,6 +98,7 @@ const tokenTypeToUpdatePermission: (tokenType: ApiTokenType) => string = (
case ApiTokenType.ADMIN: case ApiTokenType.ADMIN:
return ADMIN; return ADMIN;
case ApiTokenType.CLIENT: case ApiTokenType.CLIENT:
case ApiTokenType.BACKEND:
return UPDATE_CLIENT_API_TOKEN; return UPDATE_CLIENT_API_TOKEN;
case ApiTokenType.FRONTEND: case ApiTokenType.FRONTEND:
return UPDATE_FRONTEND_API_TOKEN; return UPDATE_FRONTEND_API_TOKEN;
@ -110,6 +112,7 @@ const tokenTypeToDeletePermission: (tokenType: ApiTokenType) => string = (
case ApiTokenType.ADMIN: case ApiTokenType.ADMIN:
return ADMIN; return ADMIN;
case ApiTokenType.CLIENT: case ApiTokenType.CLIENT:
case ApiTokenType.BACKEND:
return DELETE_CLIENT_API_TOKEN; return DELETE_CLIENT_API_TOKEN;
case ApiTokenType.FRONTEND: case ApiTokenType.FRONTEND:
return DELETE_FRONTEND_API_TOKEN; return DELETE_FRONTEND_API_TOKEN;

View File

@ -10,7 +10,11 @@ export const createApiToken = joi
.string() .string()
.lowercase() .lowercase()
.required() .required()
.valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND), .valid(
ApiTokenType.CLIENT,
ApiTokenType.BACKEND,
ApiTokenType.FRONTEND,
),
expiresAt: joi.date().optional(), expiresAt: joi.date().optional(),
projects: joi.array().min(1).optional().default([ALL]), projects: joi.array().min(1).optional().default([ALL]),
environment: joi.string().optional().default('development'), environment: joi.string().optional().default('development'),

View File

@ -10,10 +10,20 @@ export const createProjectApiToken = joi
.string() .string()
.lowercase() .lowercase()
.required() .required()
.valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND), .valid(
ApiTokenType.CLIENT,
ApiTokenType.BACKEND,
ApiTokenType.FRONTEND,
),
expiresAt: joi.date().optional(), expiresAt: joi.date().optional(),
environment: joi.when('type', { 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), then: joi.string().optional().default(DEFAULT_ENV),
}), }),
}) })

View File

@ -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", "Should prevent you from creating %s tokens when you're already at the limit",
async (tokenType) => { async (tokenType) => {
const limit = 1; const limit = 1;
@ -58,7 +63,7 @@ test.each([ApiTokenType.ADMIN, ApiTokenType.CLIENT, ApiTokenType.FRONTEND])(
await service.createApiTokenWithProjects( await service.createApiTokenWithProjects(
{ {
tokenName: 'token-1', tokenName: `token-1-${tokenType}`,
type: ApiTokenType.CLIENT, type: ApiTokenType.CLIENT,
environment: 'production', environment: 'production',
projects: ['*'], projects: ['*'],

View File

@ -5,9 +5,9 @@ import type { IUnleashStores } from '../types/stores.js';
import type { IUnleashConfig } from '../types/option.js'; import type { IUnleashConfig } from '../types/option.js';
import ApiUser, { type IApiUser } from '../types/api-user.js'; import ApiUser, { type IApiUser } from '../types/api-user.js';
import { import {
ALL,
resolveValidProjects, resolveValidProjects,
validateApiToken, validateApiToken,
validateApiTokenEnvironment,
} from '../types/models/api-token.js'; } from '../types/models/api-token.js';
import type { IApiTokenStore } from '../types/stores/api-token-store.js'; import type { IApiTokenStore } from '../types/stores/api-token-store.js';
import { FOREIGN_KEY_VIOLATION } from '../error/db-error.js'; import { FOREIGN_KEY_VIOLATION } from '../error/db-error.js';
@ -40,7 +40,10 @@ const resolveTokenPermissions = (tokenType: string) => {
return [ADMIN]; return [ADMIN];
} }
if (tokenType === ApiTokenType.CLIENT) { if (
tokenType === ApiTokenType.BACKEND ||
tokenType === ApiTokenType.CLIENT
) {
return [CLIENT]; return [CLIENT];
} }
@ -295,9 +298,7 @@ export class ApiTokenService {
auditUser: IAuditUser, auditUser: IAuditUser,
): Promise<IApiToken> { ): Promise<IApiToken> {
validateApiToken(newToken); validateApiToken(newToken);
const environments = await this.environmentStore.getAll(); await this.validateApiTokenEnvironment(newToken);
validateApiTokenEnvironment(newToken, environments);
await this.validateApiTokenLimit(); await this.validateApiTokenLimit();
const secret = this.generateSecretKey(newToken); const secret = this.generateSecretKey(newToken);
@ -305,6 +306,19 @@ export class ApiTokenService {
return this.insertNewApiToken(createNewToken, auditUser); return this.insertNewApiToken(createNewToken, auditUser);
} }
private async validateApiTokenEnvironment({
environment,
}: Pick<IApiTokenCreate, 'environment'>): Promise<void> {
if (environment === ALL) {
return;
}
const exists = await this.environmentStore.exists(environment);
if (!exists) {
throw new BadDataError(`Environment=${environment} does not exist`);
}
}
private async validateApiTokenLimit() { private async validateApiTokenLimit() {
const currentTokenCount = await this.store.count(); const currentTokenCount = await this.store.count();
const limit = this.resourceLimits.apiTokens; const limit = this.resourceLimits.apiTokens;

View File

@ -197,9 +197,11 @@ export interface IFeatureDependency {
export type IStrategyVariant = Omit<IVariant, 'overrides'>; export type IStrategyVariant = Omit<IVariant, 'overrides'>;
export enum ApiTokenType { export enum ApiTokenType {
/** @deprecated: Use BACKEND instead */
CLIENT = 'client', CLIENT = 'client',
ADMIN = 'admin', ADMIN = 'admin',
FRONTEND = 'frontend', FRONTEND = 'frontend',
BACKEND = 'backend',
} }
export interface IApiTokenCreate { export interface IApiTokenCreate {

View File

@ -1,5 +1,5 @@
import BadDataError from '../../error/bad-data-error.js'; 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'; import { ApiTokenType } from '../model.js';
export const ALL = '*'; 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( throw new BadDataError(
'Client token cannot be scoped to all environments', 'Client token cannot be scoped to all environments',
); );
@ -45,20 +48,3 @@ export const validateApiToken = ({
); );
} }
}; };
export const validateApiTokenEnvironment = (
{ environment }: Pick<IApiTokenCreate, 'environment'>,
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`);
}
};

View File

@ -1,6 +1,6 @@
// Special // Special
export const ADMIN = 'ADMIN'; 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 FRONTEND = 'FRONTEND';
export const NONE = 'NONE'; export const NONE = 'NONE';

View File

@ -40,16 +40,6 @@ afterEach(async () => {
await stores.apiTokenStore.deleteAll(); 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 () => { test('editor users should only get client or frontend tokens', async () => {
expect.assertions(3); expect.assertions(3);
@ -77,7 +67,7 @@ test('editor users should only get client or frontend tokens', async () => {
projects: [], projects: [],
tokenName: 'test', tokenName: 'test',
secret: '*:environment.1234', secret: '*:environment.1234',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
}); });
await stores.apiTokenStore.insert({ await stores.apiTokenStore.insert({
@ -102,7 +92,7 @@ test('editor users should only get client or frontend tokens', async () => {
.expect(200) .expect(200)
.expect((res) => { .expect((res) => {
expect(res.body.tokens.length).toBe(2); 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); 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: [], projects: [],
tokenName: 'test', tokenName: 'test',
secret: '*:environment.1234', secret: '*:environment.1234',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
}); });
await stores.apiTokenStore.insert({ await stores.apiTokenStore.insert({
@ -155,8 +145,10 @@ test('viewer users should not be allowed to fetch tokens', async () => {
await destroy(); await destroy();
}); });
test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', async () => { test.each(['client', 'backend'])(
expect.assertions(0); 'A role with only CREATE_PROJECT_API_TOKEN can create project %s token',
async (type) => {
expect.assertions(1);
const preHook = ( const preHook = (
app, app,
@ -172,15 +164,15 @@ test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', asyn
))!; ))!;
const user = await userService.createUser( const user = await userService.createUser(
{ {
email: 'powerpuffgirls_viewer@example.com', email: `powerpuffgirls_viewer_${type}@example.com`,
rootRole: role.id, rootRole: role.id,
}, },
SYSTEM_USER_AUDIT, SYSTEM_USER_AUDIT,
); );
const createClientApiTokenRole = await accessService.createRole( const createClientApiTokenRole = await accessService.createRole(
{ {
name: 'project_client_token_creator', name: `project_client_${type}_token_creator`,
description: 'Can create client tokens', description: `Can create ${type} tokens`,
permissions: [{ name: CREATE_PROJECT_API_TOKEN }], permissions: [{ name: CREATE_PROJECT_API_TOKEN }],
type: 'root-custom', type: 'root-custom',
createdByUserId: SYSTEM_USER_ID, createdByUserId: SYSTEM_USER_ID,
@ -204,17 +196,19 @@ test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', asyn
db.rawDatabase, db.rawDatabase,
); );
await request const { body, status } = await request
.post('/api/admin/projects/default/api-tokens') .post('/api/admin/projects/default/api-tokens')
.send({ .send({
tokenName: 'client-token-maker', tokenName: `${type}-token-maker`,
type: 'client', type,
projects: ['default'], projects: ['default'],
}) })
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json');
.expect(201); console.log(`Response: ${JSON.stringify(body)}`);
expect(status).toBe(201);
await destroy(); await destroy();
}); },
);
describe('Fine grained API token permissions', () => { describe('Fine grained API token permissions', () => {
describe('A role with access to CREATE_CLIENT_API_TOKEN', () => { describe('A role with access to CREATE_CLIENT_API_TOKEN', () => {
@ -468,7 +462,7 @@ describe('Fine grained API token permissions', () => {
projects: [], projects: [],
tokenName: 'client', tokenName: 'client',
secret: '*:environment.client_secret_1234', secret: '*:environment.client_secret_1234',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
}); });
await stores.apiTokenStore.insert({ await stores.apiTokenStore.insert({
@ -491,7 +485,7 @@ describe('Fine grained API token permissions', () => {
.expect(200) .expect(200)
.expect((res) => { .expect((res) => {
expect(res.body.tokens).toHaveLength(1); 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(); await destroy();
}); });
@ -527,7 +521,7 @@ describe('Fine grained API token permissions', () => {
projects: [], projects: [],
tokenName: 'client', tokenName: 'client',
secret: '*:environment.client_secret_4321', secret: '*:environment.client_secret_4321',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
}); });
await stores.apiTokenStore.insert({ await stores.apiTokenStore.insert({
@ -585,7 +579,7 @@ describe('Fine grained API token permissions', () => {
projects: [], projects: [],
tokenName: 'client', tokenName: 'client',
secret: '*:environment.client_secret_4321', secret: '*:environment.client_secret_4321',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
}); });
await stores.apiTokenStore.insert({ await stores.apiTokenStore.insert({
environment: '', environment: '',

View File

@ -48,18 +48,18 @@ test('returns empty list of tokens', async () => {
}); });
}); });
test('creates new client token', async () => { test.each(['client', 'backend'])('creates new %s token', async (type) => {
return app.request await app.request
.post('/api/admin/api-tokens') .post('/api/admin/api-tokens')
.send({ .send({
tokenName: 'default-client', tokenName: 'default-client',
type: 'client', type,
}) })
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.expect(201) .expect(201)
.expect((res) => { .expect((res) => {
expect(res.body.tokenName).toBe('default-client'); 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.createdAt).toBeTruthy();
expect(res.body.secret.length > 16).toBe(true); expect(res.body.secret.length > 16).toBe(true);
}); });
@ -72,7 +72,7 @@ test('update client token with expiry', async () => {
projects: ['*'], projects: ['*'],
tokenName: 'test_token', tokenName: 'test_token',
secret: tokenSecret, secret: tokenSecret,
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
environment: 'development', environment: 'development',
}); });
@ -94,7 +94,9 @@ test('update client token with expiry', async () => {
}); });
}); });
test('creates a lot of client tokens', async () => { test.each(['client', 'backend'])(
'creates a lot of backend tokens from type %s',
async (type) => {
const requests: any[] = []; const requests: any[] = [];
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
@ -103,7 +105,7 @@ test('creates a lot of client tokens', async () => {
.post('/api/admin/api-tokens') .post('/api/admin/api-tokens')
.send({ .send({
tokenName: 'default-client', tokenName: 'default-client',
type: 'client', type,
}) })
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.expect(201), .expect(201),
@ -117,7 +119,7 @@ test('creates a lot of client tokens', async () => {
.expect(200) .expect(200)
.expect((res) => { .expect((res) => {
expect(res.body.tokens.length).toBe(10); expect(res.body.tokens.length).toBe(10);
expect(res.body.tokens[2].type).toBe('client'); expect(res.body.tokens[2].type).toBe(type);
}); });
await app.request await app.request
.get('/api/admin/api-tokens/default-client') .get('/api/admin/api-tokens/default-client')
@ -125,9 +127,10 @@ test('creates a lot of client tokens', async () => {
.expect(200) .expect(200)
.expect((res) => { .expect((res) => {
expect(res.body.tokens.length).toBe(10); expect(res.body.tokens.length).toBe(10);
expect(res.body.tokens[2].type).toBe('client'); expect(res.body.tokens[2].type).toBe(type);
});
}); });
},
);
test('removes api token', async () => { test('removes api token', async () => {
const tokenSecret = '*:environment.random-secret'; const tokenSecret = '*:environment.random-secret';
@ -137,7 +140,7 @@ test('removes api token', async () => {
projects: ['*'], projects: ['*'],
tokenName: 'testtoken', tokenName: 'testtoken',
secret: tokenSecret, secret: tokenSecret,
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
}); });
await app.request await app.request
@ -154,41 +157,47 @@ test('removes api token', async () => {
}); });
}); });
test('creates new client token: project & environment defaults to "*"', async () => { test.each(['client', 'backend'])(
return app.request 'creates new %s token: project & environment defaults to "*"',
async (type) => {
await app.request
.post('/api/admin/api-tokens') .post('/api/admin/api-tokens')
.send({ .send({
tokenName: 'default-client', tokenName: 'default-client',
type: 'client', type,
}) })
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.expect(201) .expect(201)
.expect((res) => { .expect((res) => {
expect(res.body.type).toBe('client'); expect(res.body.type).toBe(type);
expect(res.body.secret.length > 16).toBe(true); expect(res.body.secret.length > 16).toBe(true);
expect(res.body.environment).toBe(DEFAULT_ENV); expect(res.body.environment).toBe(DEFAULT_ENV);
expect(res.body.projects[0]).toBe(ALL); expect(res.body.projects[0]).toBe(ALL);
}); });
}); },
);
test('creates new client token with project & environment set', async () => { test.each(['client', 'backend'])(
return app.request 'creates new %s token with project & environment set',
async (type) => {
await app.request
.post('/api/admin/api-tokens') .post('/api/admin/api-tokens')
.send({ .send({
tokenName: 'default-client', tokenName: 'default-client',
type: 'client', type,
projects: ['default'], projects: ['default'],
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
}) })
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.expect(201) .expect(201)
.expect((res) => { .expect((res) => {
expect(res.body.type).toBe('client'); expect(res.body.type).toBe(type);
expect(res.body.secret.length > 16).toBe(true); expect(res.body.secret.length > 16).toBe(true);
expect(res.body.environment).toBe(DEFAULT_ENV); expect(res.body.environment).toBe(DEFAULT_ENV);
expect(res.body.projects[0]).toBe('default'); expect(res.body.projects[0]).toBe('default');
}); });
}); },
);
test('should prefix default token with "*:*."', async () => { test('should prefix default token with "*:*."', async () => {
return app.request 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') .delete('/api/admin/api-tokens/random-non-existing-token')
.expect(200); .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: ['*'],
});
});

View File

@ -63,7 +63,7 @@ beforeAll(async () => {
defaultToken = defaultToken =
await app.services.apiTokenService.createApiTokenWithProjects({ await app.services.apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
projects: ['default'], projects: ['default'],
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
tokenName: 'tester', tokenName: 'tester',

View File

@ -77,7 +77,7 @@ test('api tokens are serialized correctly', async () => {
}); });
await app.services.apiTokenService.createApiTokenWithProjects({ await app.services.apiTokenService.createApiTokenWithProjects({
tokenName: 'client', tokenName: 'client',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
projects: ['*'], projects: ['*'],
}); });
@ -88,7 +88,7 @@ test('api tokens are serialized correctly', async () => {
.expect(200); .expect(200);
expect(body).toMatchObject({ expect(body).toMatchObject({
apiTokens: { client: 1, admin: 1, frontend: 1 }, apiTokens: { backend: 1, admin: 1, frontend: 1 },
}); });
const { text: csv } = await app.request const { text: csv } = await app.request
@ -96,7 +96,7 @@ test('api tokens are serialized correctly', async () => {
.expect('Content-Type', /text\/csv/) .expect('Content-Type', /text\/csv/)
.expect(200); .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 () => { test('should return instance statistics with correct number of projects', async () => {

View File

@ -153,7 +153,7 @@ test('should save multiple projects from token', async () => {
const multiProjectToken = const multiProjectToken =
await app.services.apiTokenService.createApiTokenWithProjects({ await app.services.apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
projects: ['default', 'mainProject'], projects: ['default', 'mainProject'],
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
tokenName: 'tester', tokenName: 'tester',

View File

@ -50,7 +50,7 @@ test('Returns list of tokens', async () => {
await db.stores.apiTokenStore.insert({ await db.stores.apiTokenStore.insert({
tokenName: 'test', tokenName: 'test',
secret: tokenSecret, secret: tokenSecret,
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
projects: ['default'], projects: ['default'],
}); });
@ -87,21 +87,23 @@ test('fails to create new client token when given wrong project', async () => {
.expect(404); .expect(404);
}); });
test('creates new client token', async () => { test.each(['client', 'frontend', 'backend'])(
return app.request 'creates new %s token',
async (type) => {
const { body, status } = await app.request
.post('/api/admin/projects/default/api-tokens') .post('/api/admin/projects/default/api-tokens')
.send({ .send({
tokenName: 'default-client', tokenName: `default-${type}`,
type: 'client', type,
projects: ['default'], projects: ['default'],
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
}) })
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json');
.expect(201) console.log(body);
.expect((res) => { expect(status).toBe(201);
expect(res.body.tokenName).toBe('default-client'); expect(body.tokenName).toBe(`default-${type}`);
}); },
}); );
test('Deletes existing tokens', async () => { test('Deletes existing tokens', async () => {
const tokenSecret = 'random-secret'; const tokenSecret = 'random-secret';
@ -109,7 +111,7 @@ test('Deletes existing tokens', async () => {
await db.stores.apiTokenStore.insert({ await db.stores.apiTokenStore.insert({
tokenName: 'test', tokenName: 'test',
secret: tokenSecret, secret: tokenSecret,
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
projects: ['default'], projects: ['default'],
}); });
@ -142,7 +144,7 @@ test('Returns Bad Request when deleting tokens with more than one project', asyn
await db.stores.apiTokenStore.insert({ await db.stores.apiTokenStore.insert({
tokenName: 'test', tokenName: 'test',
secret: tokenSecret, secret: tokenSecret,
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
projects: ['default', 'other'], projects: ['default', 'other'],
}); });

View File

@ -67,7 +67,7 @@ test('Access with API token is granted', async () => {
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
projects: ['default'], projects: ['default'],
tokenName: 'test', tokenName: 'test',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
}, },
); );
await app.request await app.request

View File

@ -61,7 +61,7 @@ beforeAll(async () => {
const token = await app.services.apiTokenService.createApiTokenWithProjects( const token = await app.services.apiTokenService.createApiTokenWithProjects(
{ {
tokenName: 'test', tokenName: 'test',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
projects: ['default'], projects: ['default'],
}, },

View File

@ -126,7 +126,7 @@ afterAll(async () => {
test('returns feature flag with "default" config', async () => { test('returns feature flag with "default" config', async () => {
const token = await apiTokenService.createApiTokenWithProjects({ const token = await apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
tokenName, tokenName,
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
projects: [project], projects: [project],
@ -148,7 +148,7 @@ test('returns feature flag with "default" config', async () => {
test('returns feature flag with testing environment config', async () => { test('returns feature flag with testing environment config', async () => {
const token = await apiTokenService.createApiTokenWithProjects({ const token = await apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
tokenName: tokenName, tokenName: tokenName,
environment, environment,
projects: [project], projects: [project],
@ -174,7 +174,7 @@ test('returns feature flag with testing environment config', async () => {
test('returns feature flag for project2', async () => { test('returns feature flag for project2', async () => {
const token = await apiTokenService.createApiTokenWithProjects({ const token = await apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
tokenName: tokenName, tokenName: tokenName,
environment, environment,
projects: [project2], projects: [project2],
@ -194,7 +194,7 @@ test('returns feature flag for project2', async () => {
test('returns feature flag for all projects', async () => { test('returns feature flag for all projects', async () => {
const token = await apiTokenService.createApiTokenWithProjects({ const token = await apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
tokenName: tokenName, tokenName: tokenName,
environment, environment,
projects: ['*'], projects: ['*'],

View File

@ -134,7 +134,7 @@ afterAll(async () => {
test('doesnt return feature flags if project deleted', async () => { test('doesnt return feature flags if project deleted', async () => {
const token = await apiTokenService.createApiTokenWithProjects({ const token = await apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
tokenName: deletionTokenName, tokenName: deletionTokenName,
environment, environment,
projects: [deletionProject], projects: [deletionProject],

View File

@ -32,7 +32,7 @@ test('should enrich metrics with environment from api-token', async () => {
}); });
const token = await apiTokenService.createApiTokenWithProjects({ const token = await apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
tokenName: 'test', tokenName: 'test',
environment: 'some', environment: 'some',
projects: ['*'], projects: ['*'],

View File

@ -20,7 +20,7 @@ beforeAll(async () => {
app = await setupAppWithAuth(db.stores, {}, db.rawDatabase); app = await setupAppWithAuth(db.stores, {}, db.rawDatabase);
defaultToken = defaultToken =
await app.services.apiTokenService.createApiTokenWithProjects({ await app.services.apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
projects: ['default'], projects: ['default'],
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
tokenName: 'tester', tokenName: 'tester',
@ -75,7 +75,7 @@ test('should pick up environment from token', async () => {
await db.stores.environmentStore.create({ name: 'test', type: 'test' }); await db.stores.environmentStore.create({ name: 'test', type: 'test' });
const token = await app.services.apiTokenService.createApiTokenWithProjects( const token = await app.services.apiTokenService.createApiTokenWithProjects(
{ {
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
projects: ['default'], projects: ['default'],
environment, environment,
tokenName: 'tester', 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( const token = await app.services.apiTokenService.createApiTokenWithProjects(
{ {
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
projects: ['default'], projects: ['default'],
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
tokenName: 'tester', tokenName: 'tester',

View File

@ -70,7 +70,7 @@ test('should have empty list of tokens', async () => {
test('should create client token', async () => { test('should create client token', async () => {
const token = await apiTokenService.createApiTokenWithProjects({ const token = await apiTokenService.createApiTokenWithProjects({
tokenName: 'default-client', tokenName: 'default-client',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
projects: ['*'], projects: ['*'],
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
}); });
@ -78,7 +78,7 @@ test('should create client token', async () => {
expect(allTokens.length).toBe(1); expect(allTokens.length).toBe(1);
expect(token.secret.length > 32).toBe(true); expect(token.secret.length > 32).toBe(true);
expect(token.type).toBe(ApiTokenType.CLIENT); expect(token.type).toBe(ApiTokenType.BACKEND);
expect(token.tokenName).toBe('default-client'); expect(token.tokenName).toBe('default-client');
expect(allTokens[0].secret).toBe(token.secret); 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'); const time = new Date('2022-01-01');
await apiTokenService.createApiTokenWithProjects({ await apiTokenService.createApiTokenWithProjects({
tokenName: 'default-client', tokenName: 'default-client',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
expiresAt: time, expiresAt: time,
projects: ['*'], projects: ['*'],
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
@ -117,7 +117,7 @@ test('should update expiry of token', async () => {
const token = await apiTokenService.createApiTokenWithProjects( const token = await apiTokenService.createApiTokenWithProjects(
{ {
tokenName: 'default-client', tokenName: 'default-client',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
expiresAt: time, expiresAt: time,
projects: ['*'], projects: ['*'],
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
@ -135,7 +135,7 @@ test('should update expiry of token', async () => {
test('should create client token with project list', async () => { test('should create client token with project list', async () => {
const token = await apiTokenService.createApiTokenWithProjects({ const token = await apiTokenService.createApiTokenWithProjects({
tokenName: 'default-client', tokenName: 'default-client',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
projects: ['default', 'test-project'], projects: ['default', 'test-project'],
environment: DEFAULT_ENV, 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 () => { test('should strip all other projects if ALL_PROJECTS is present', async () => {
const token = await apiTokenService.createApiTokenWithProjects({ const token = await apiTokenService.createApiTokenWithProjects({
tokenName: 'default-client', tokenName: 'default-client',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
projects: ['*', 'default'], projects: ['*', 'default'],
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
}); });
@ -162,7 +162,7 @@ test('should return user with multiple projects', async () => {
const { secret: secret1 } = const { secret: secret1 } =
await apiTokenService.createApiTokenWithProjects({ await apiTokenService.createApiTokenWithProjects({
tokenName: 'default-valid', tokenName: 'default-valid',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
expiresAt: tomorrow, expiresAt: tomorrow,
projects: ['test-project', 'default'], projects: ['test-project', 'default'],
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
@ -171,7 +171,7 @@ test('should return user with multiple projects', async () => {
const { secret: secret2 } = const { secret: secret2 } =
await apiTokenService.createApiTokenWithProjects({ await apiTokenService.createApiTokenWithProjects({
tokenName: 'default-also-valid', tokenName: 'default-also-valid',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
expiresAt: tomorrow, expiresAt: tomorrow,
projects: ['test-project'], projects: ['test-project'],
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
@ -191,7 +191,7 @@ test('should not partially create token if projects are invalid', async () => {
try { try {
await apiTokenService.createApiTokenWithProjects({ await apiTokenService.createApiTokenWithProjects({
tokenName: 'default-client', tokenName: 'default-client',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
projects: ['non-existent-project'], projects: ['non-existent-project'],
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
}); });

View File

@ -69,7 +69,7 @@ test('should only return valid tokens', async () => {
const expiredToken = await stores.apiTokenStore.insert({ const expiredToken = await stores.apiTokenStore.insert({
tokenName: 'expired', tokenName: 'expired',
secret: '*:environment.expired-secret', secret: '*:environment.expired-secret',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
expiresAt: yesterday, expiresAt: yesterday,
projects: ['*'], projects: ['*'],
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
@ -78,7 +78,7 @@ test('should only return valid tokens', async () => {
const activeToken = await stores.apiTokenStore.insert({ const activeToken = await stores.apiTokenStore.insert({
tokenName: 'default-valid', tokenName: 'default-valid',
secret: '*:environment.valid-secret', secret: '*:environment.valid-secret',
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
expiresAt: tomorrow, expiresAt: tomorrow,
projects: ['*'], projects: ['*'],
environment: DEFAULT_ENV, environment: DEFAULT_ENV,

View File

@ -57,14 +57,14 @@ describe('count deprecated tokens', () => {
await stores.apiTokenStore.insert({ await stores.apiTokenStore.insert({
secret: 'default:development.be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178', secret: 'default:development.be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178',
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
projects: ['default'], projects: ['default'],
tokenName: 'client-token', tokenName: 'client-token',
}); });
await stores.apiTokenStore.insert({ await stores.apiTokenStore.insert({
secret: '*:development.be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178', secret: '*:development.be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178',
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
projects: [], projects: [],
tokenName: 'client-wildcard-token', tokenName: 'client-wildcard-token',
}); });
@ -111,7 +111,7 @@ describe('count deprecated tokens', () => {
await stores.apiTokenStore.insert({ await stores.apiTokenStore.insert({
secret: 'deleted-project:development.be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178', secret: 'deleted-project:development.be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178',
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
projects: [], projects: [],
tokenName: 'admin-test-token', tokenName: 'admin-test-token',
}); });
@ -131,7 +131,7 @@ describe('count deprecated tokens', () => {
await stores.apiTokenStore.insert({ await stores.apiTokenStore.insert({
secret: '*:*.be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178', secret: '*:*.be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178',
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
projects: [], projects: [],
tokenName: 'client-test-token', tokenName: 'client-test-token',
}); });
@ -195,14 +195,14 @@ describe('count project tokens', () => {
await store.insert({ await store.insert({
secret: `default:default.${randomId()}`, secret: `default:default.${randomId()}`,
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
projects: ['default'], projects: ['default'],
tokenName: 'token1', tokenName: 'token1',
}); });
await store.insert({ await store.insert({
secret: `*:*.${randomId()}`, secret: `*:*.${randomId()}`,
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
projects: ['*'], projects: ['*'],
tokenName: 'token2', tokenName: 'token2',
}); });
@ -210,7 +210,7 @@ describe('count project tokens', () => {
await store.insert({ await store.insert({
secret: `${project.id}:default.${randomId()}`, secret: `${project.id}:default.${randomId()}`,
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
projects: [project.id], projects: [project.id],
tokenName: 'token3', tokenName: 'token3',
}); });
@ -218,7 +218,7 @@ describe('count project tokens', () => {
await store.insert({ await store.insert({
secret: `[]:default.${randomId()}`, secret: `[]:default.${randomId()}`,
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
type: ApiTokenType.CLIENT, type: ApiTokenType.BACKEND,
projects: [project.id, 'default'], projects: [project.id, 'default'],
tokenName: 'token4', tokenName: 'token4',
}); });

View File

@ -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_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.** | | `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). | | `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 ### Server behavior