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:
parent
02ee94c38f
commit
92480554dc
@ -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
|
||||
|
@ -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
|
||||
|
@ -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', () => {
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 = {
|
||||
|
@ -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))
|
||||
|
@ -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());
|
||||
|
@ -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(
|
||||
|
@ -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: {
|
||||
|
@ -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: {
|
||||
|
@ -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;
|
||||
|
@ -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'),
|
||||
|
@ -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),
|
||||
}),
|
||||
})
|
||||
|
@ -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: ['*'],
|
||||
|
@ -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<IApiToken> {
|
||||
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<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() {
|
||||
const currentTokenCount = await this.store.count();
|
||||
const limit = this.resourceLimits.apiTokens;
|
||||
|
@ -197,9 +197,11 @@ export interface IFeatureDependency {
|
||||
export type IStrategyVariant = Omit<IVariant, 'overrides'>;
|
||||
|
||||
export enum ApiTokenType {
|
||||
/** @deprecated: Use BACKEND instead */
|
||||
CLIENT = 'client',
|
||||
ADMIN = 'admin',
|
||||
FRONTEND = 'frontend',
|
||||
BACKEND = 'backend',
|
||||
}
|
||||
|
||||
export interface IApiTokenCreate {
|
||||
|
@ -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<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`);
|
||||
}
|
||||
};
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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: '',
|
||||
|
@ -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: ['*'],
|
||||
});
|
||||
});
|
||||
|
@ -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',
|
||||
|
@ -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 () => {
|
||||
|
@ -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',
|
||||
|
@ -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'],
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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'],
|
||||
},
|
||||
|
@ -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: ['*'],
|
||||
|
@ -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],
|
||||
|
@ -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: ['*'],
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
});
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user