1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-06 00:07:44 +01:00
unleash.unleash/src/test/e2e/services/api-token-service.e2e.test.ts

229 lines
7.0 KiB
TypeScript
Raw Normal View History

import dbInit, { type ITestDb } from '../helpers/database-init';
2021-03-29 19:58:11 +02:00
import getLogger from '../../fixtures/no-logger';
import { ApiTokenService } from '../../../lib/services/api-token-service';
import { createTestConfig } from '../../config/test-config';
import {
ApiTokenType,
type IApiToken,
} from '../../../lib/types/models/api-token';
import { DEFAULT_ENV } from '../../../lib/util/constants';
import { addDays, subDays } from 'date-fns';
import type ProjectService from '../../../lib/features/project/project-service';
2023-10-04 12:20:27 +02:00
import { createProjectService } from '../../../lib/features';
import { EventService } from '../../../lib/services';
import type { IUnleashStores } from '../../../lib/types';
2021-03-29 19:58:11 +02:00
let db: ITestDb;
let stores: IUnleashStores;
2021-03-29 19:58:11 +02:00
let apiTokenService: ApiTokenService;
let projectService: ProjectService;
2021-03-29 19:58:11 +02:00
beforeAll(async () => {
const config = createTestConfig({
server: { baseUriPath: '/test' },
experimental: {
flags: {
useMemoizedActiveTokens: true,
},
},
});
db = await dbInit('api_token_service_serial', getLogger);
2021-03-29 19:58:11 +02:00
stores = db.stores;
const eventService = new EventService(stores, config);
const project = {
id: 'test-project',
name: 'Test Project',
description: 'Fancy',
2023-03-16 15:29:52 +01:00
mode: 'open' as const,
defaultStickiness: 'clientId',
};
const user = await stores.userStore.insert({
name: 'Some Name',
email: 'test@getunleash.io',
});
2023-10-04 12:20:27 +02:00
projectService = createProjectService(db.rawDatabase, config);
await projectService.createProject(project, user);
apiTokenService = new ApiTokenService(stores, config, eventService);
2021-03-29 19:58:11 +02:00
});
afterAll(async () => {
if (db) {
await db.destroy();
}
2021-03-29 19:58:11 +02:00
});
afterEach(async () => {
2021-03-29 19:58:11 +02:00
const tokens = await stores.apiTokenStore.getAll();
const deleteAll = tokens.map((t: IApiToken) =>
stores.apiTokenStore.delete(t.secret),
);
await Promise.all(deleteAll);
});
test('should have empty list of tokens', async () => {
2021-03-29 19:58:11 +02:00
const allTokens = await apiTokenService.getAllTokens();
const activeTokens = await apiTokenService.getAllTokens();
expect(allTokens.length).toBe(0);
expect(activeTokens.length).toBe(0);
2021-03-29 19:58:11 +02:00
});
test('should create client token', async () => {
const token = await apiTokenService.createApiToken({
tokenName: 'default-client',
2021-03-29 19:58:11 +02:00
type: ApiTokenType.CLIENT,
project: '*',
environment: DEFAULT_ENV,
2021-03-29 19:58:11 +02:00
});
const allTokens = await apiTokenService.getAllTokens();
expect(allTokens.length).toBe(1);
expect(token.secret.length > 32).toBe(true);
expect(token.type).toBe(ApiTokenType.CLIENT);
expect(token.username).toBe('default-client');
expect(allTokens[0].secret).toBe(token.secret);
2021-03-29 19:58:11 +02:00
});
test('should create admin token', async () => {
const token = await apiTokenService.createApiToken({
tokenName: 'admin',
2021-03-29 19:58:11 +02:00
type: ApiTokenType.ADMIN,
project: '*',
environment: '*',
2021-03-29 19:58:11 +02:00
});
expect(token.secret.length > 32).toBe(true);
expect(token.type).toBe(ApiTokenType.ADMIN);
2021-03-29 19:58:11 +02:00
});
test('should set expiry of token', async () => {
2021-03-29 19:58:11 +02:00
const time = new Date('2022-01-01');
await apiTokenService.createApiToken({
tokenName: 'default-client',
2021-03-29 19:58:11 +02:00
type: ApiTokenType.CLIENT,
expiresAt: time,
project: '*',
environment: DEFAULT_ENV,
2021-03-29 19:58:11 +02:00
});
const [token] = await apiTokenService.getAllTokens();
expect(token.expiresAt).toEqual(time);
2021-03-29 19:58:11 +02:00
});
test('should update expiry of token', async () => {
2021-03-29 19:58:11 +02:00
const time = new Date('2022-01-01');
const newTime = new Date('2023-01-01');
const token = await apiTokenService.createApiToken(
{
tokenName: 'default-client',
type: ApiTokenType.CLIENT,
expiresAt: time,
project: '*',
environment: DEFAULT_ENV,
},
'tester',
);
2021-03-29 19:58:11 +02:00
await apiTokenService.updateExpiry(token.secret, newTime, 'tester', -9999);
2021-03-29 19:58:11 +02:00
const [updatedToken] = await apiTokenService.getAllTokens();
expect(updatedToken.expiresAt).toEqual(newTime);
2021-03-29 19:58:11 +02:00
});
test('should only return valid tokens', async () => {
const now = Date.now();
const yesterday = subDays(now, 1);
const tomorrow = addDays(now, 1);
2021-03-29 19:58:11 +02:00
await apiTokenService.createApiToken({
tokenName: 'default-expired',
2021-03-29 19:58:11 +02:00
type: ApiTokenType.CLIENT,
expiresAt: yesterday,
project: '*',
environment: DEFAULT_ENV,
2021-03-29 19:58:11 +02:00
});
const activeToken = await apiTokenService.createApiToken({
tokenName: 'default-valid',
2021-03-29 19:58:11 +02:00
type: ApiTokenType.CLIENT,
expiresAt: tomorrow,
project: '*',
environment: DEFAULT_ENV,
2021-03-29 19:58:11 +02:00
});
const tokens = await apiTokenService.getAllActiveTokens();
expect(tokens.length).toBe(1);
expect(activeToken.secret).toBe(tokens[0].secret);
2021-03-29 19:58:11 +02:00
});
test('should create client token with project list', async () => {
const token = await apiTokenService.createApiToken({
tokenName: 'default-client',
type: ApiTokenType.CLIENT,
projects: ['default', 'test-project'],
environment: DEFAULT_ENV,
});
expect(token.secret.slice(0, 2)).toEqual('[]');
expect(token.projects).toStrictEqual(['default', 'test-project']);
});
test('should strip all other projects if ALL_PROJECTS is present', async () => {
const token = await apiTokenService.createApiToken({
tokenName: 'default-client',
type: ApiTokenType.CLIENT,
projects: ['*', 'default'],
environment: DEFAULT_ENV,
});
expect(token.projects).toStrictEqual(['*']);
});
test('should return user with multiple projects', async () => {
const now = Date.now();
const tomorrow = addDays(now, 1);
const { secret: secret1 } = await apiTokenService.createApiToken({
tokenName: 'default-valid',
type: ApiTokenType.CLIENT,
expiresAt: tomorrow,
projects: ['test-project', 'default'],
environment: DEFAULT_ENV,
});
const { secret: secret2 } = await apiTokenService.createApiToken({
tokenName: 'default-also-valid',
type: ApiTokenType.CLIENT,
expiresAt: tomorrow,
projects: ['test-project'],
environment: DEFAULT_ENV,
});
feat: allow api token middleware to fetch from db (#6344) ## About the changes When edge is configured to automatically generate tokens, it requires the token to be present in all unleash instances. It's behind a flag which enables us to turn it on on a case by case scenario. The risk of this implementation is that we'd be adding load to the database in the middleware that evaluates tokens (which are present in mostly all our API calls. We only query when the token is missing but because the /client and /frontend endpoints which will be the affected ones are high throughput, we want to be extra careful to avoid DDoSing ourselves ## Alternatives: One alternative would be that we merge the two endpoints into one. Currently, Edge does the following: If the token is not valid, it tries to create a token using a service account token and /api/admin/create-token endpoint. Then it uses the token generated (which is returned from the prior endpoint) to query /api/frontend. What if we could call /api/frontend with the same service account we use to create the token? It may sound risky but if the same application holding the service account token with permission to create a token, can call /api/frontend via the generated token, shouldn't it be able to call the endpoint directly? The purpose of the token is authentication and authorization. With the two tokens we are authenticating the same app with 2 different authorization scopes, but because it's the same app we are authenticating, can't we just use one token and assume that the app has both scopes? If the service account already has permissions to create a token and then use that token for further actions, allowing it to directly call /api/frontend does not necessarily introduce new security risks. The only risk is allowing the app to generate new tokens. Which leads to the third alternative: should we just remove this option from edge?
2024-02-27 16:08:44 +01:00
const multiProjectUser = await apiTokenService.getUserForToken(secret1);
const singleProjectUser = await apiTokenService.getUserForToken(secret2);
2023-03-16 15:29:52 +01:00
expect(multiProjectUser!.projects).toStrictEqual([
'test-project',
'default',
]);
2023-03-16 15:29:52 +01:00
expect(singleProjectUser!.projects).toStrictEqual(['test-project']);
});
test('should not partially create token if projects are invalid', async () => {
try {
await apiTokenService.createApiTokenWithProjects({
tokenName: 'default-client',
type: ApiTokenType.CLIENT,
projects: ['non-existent-project'],
environment: DEFAULT_ENV,
});
} catch (e) {}
const allTokens = await apiTokenService.getAllTokens();
expect(allTokens.length).toBe(0);
});