mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	chore(apitoken)!: remove ILegacyApiTokenCreate (#10072)
This commit is contained in:
		
							parent
							
								
									37548c3436
								
							
						
					
					
						commit
						1aadbb3641
					
				@ -27,7 +27,7 @@ test('should create default config', async () => {
 | 
				
			|||||||
test('should add initApiToken for admin token from options', async () => {
 | 
					test('should add initApiToken for admin token from options', async () => {
 | 
				
			||||||
    const token = {
 | 
					    const token = {
 | 
				
			||||||
        environment: '*',
 | 
					        environment: '*',
 | 
				
			||||||
        project: '*',
 | 
					        projects: ['*'],
 | 
				
			||||||
        secret: '*:*.some-random-string',
 | 
					        secret: '*:*.some-random-string',
 | 
				
			||||||
        type: ApiTokenType.ADMIN,
 | 
					        type: ApiTokenType.ADMIN,
 | 
				
			||||||
        tokenName: 'admin',
 | 
					        tokenName: 'admin',
 | 
				
			||||||
@ -52,7 +52,9 @@ test('should add initApiToken for admin token from options', async () => {
 | 
				
			|||||||
    expect(config.authentication.initApiTokens[0].environment).toBe(
 | 
					    expect(config.authentication.initApiTokens[0].environment).toBe(
 | 
				
			||||||
        token.environment,
 | 
					        token.environment,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    expect(config.authentication.initApiTokens[0].project).toBe(token.project);
 | 
					    expect(config.authentication.initApiTokens[0].projects).toMatchObject(
 | 
				
			||||||
 | 
					        token.projects,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
    expect(config.authentication.initApiTokens[0].type).toBe(
 | 
					    expect(config.authentication.initApiTokens[0].type).toBe(
 | 
				
			||||||
        ApiTokenType.ADMIN,
 | 
					        ApiTokenType.ADMIN,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@ -61,7 +63,7 @@ test('should add initApiToken for admin token from options', async () => {
 | 
				
			|||||||
test('should add initApiToken for client token from options', async () => {
 | 
					test('should add initApiToken for client token from options', async () => {
 | 
				
			||||||
    const token = {
 | 
					    const token = {
 | 
				
			||||||
        environment: 'development',
 | 
					        environment: 'development',
 | 
				
			||||||
        project: 'default',
 | 
					        projects: ['default'],
 | 
				
			||||||
        secret: 'default:development.some-random-string',
 | 
					        secret: 'default:development.some-random-string',
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
        tokenName: 'admin',
 | 
					        tokenName: 'admin',
 | 
				
			||||||
@ -86,7 +88,9 @@ test('should add initApiToken for client token from options', async () => {
 | 
				
			|||||||
    expect(config.authentication.initApiTokens[0].environment).toBe(
 | 
					    expect(config.authentication.initApiTokens[0].environment).toBe(
 | 
				
			||||||
        token.environment,
 | 
					        token.environment,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    expect(config.authentication.initApiTokens[0].project).toBe(token.project);
 | 
					    expect(config.authentication.initApiTokens[0].projects).toMatchObject(
 | 
				
			||||||
 | 
					        token.projects,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
    expect(config.authentication.initApiTokens[0].type).toBe(
 | 
					    expect(config.authentication.initApiTokens[0].type).toBe(
 | 
				
			||||||
        ApiTokenType.CLIENT,
 | 
					        ApiTokenType.CLIENT,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@ -110,7 +114,9 @@ test('should add initApiToken for admin token from env var', async () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    expect(config.authentication.initApiTokens).toHaveLength(2);
 | 
					    expect(config.authentication.initApiTokens).toHaveLength(2);
 | 
				
			||||||
    expect(config.authentication.initApiTokens[0].environment).toBe('*');
 | 
					    expect(config.authentication.initApiTokens[0].environment).toBe('*');
 | 
				
			||||||
    expect(config.authentication.initApiTokens[0].project).toBe('*');
 | 
					    expect(config.authentication.initApiTokens[0].projects).toMatchObject([
 | 
				
			||||||
 | 
					        '*',
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
    expect(config.authentication.initApiTokens[0].type).toBe(
 | 
					    expect(config.authentication.initApiTokens[0].type).toBe(
 | 
				
			||||||
        ApiTokenType.ADMIN,
 | 
					        ApiTokenType.ADMIN,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@ -146,7 +152,7 @@ test('should merge initApiToken from options and env vars', async () => {
 | 
				
			|||||||
    process.env.INIT_CLIENT_API_TOKENS = 'default:development.some-token1';
 | 
					    process.env.INIT_CLIENT_API_TOKENS = 'default:development.some-token1';
 | 
				
			||||||
    const token = {
 | 
					    const token = {
 | 
				
			||||||
        environment: '*',
 | 
					        environment: '*',
 | 
				
			||||||
        project: '*',
 | 
					        projects: ['*'],
 | 
				
			||||||
        secret: '*:*.some-random-string',
 | 
					        secret: '*:*.some-random-string',
 | 
				
			||||||
        type: ApiTokenType.ADMIN,
 | 
					        type: ApiTokenType.ADMIN,
 | 
				
			||||||
        tokenName: 'admin',
 | 
					        tokenName: 'admin',
 | 
				
			||||||
@ -193,7 +199,9 @@ test('should add initApiToken for client token from env var', async () => {
 | 
				
			|||||||
    expect(config.authentication.initApiTokens[0].environment).toBe(
 | 
					    expect(config.authentication.initApiTokens[0].environment).toBe(
 | 
				
			||||||
        'development',
 | 
					        'development',
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    expect(config.authentication.initApiTokens[0].project).toBe('default');
 | 
					    expect(config.authentication.initApiTokens[0].projects).toMatchObject([
 | 
				
			||||||
 | 
					        'default',
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
    expect(config.authentication.initApiTokens[0].type).toBe(
 | 
					    expect(config.authentication.initApiTokens[0].type).toBe(
 | 
				
			||||||
        ApiTokenType.CLIENT,
 | 
					        ApiTokenType.CLIENT,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@ -207,7 +215,7 @@ test('should add initApiToken for client token from env var', async () => {
 | 
				
			|||||||
test('should handle cases where no env var specified for tokens', async () => {
 | 
					test('should handle cases where no env var specified for tokens', async () => {
 | 
				
			||||||
    const token = {
 | 
					    const token = {
 | 
				
			||||||
        environment: '*',
 | 
					        environment: '*',
 | 
				
			||||||
        project: '*',
 | 
					        projects: ['*'],
 | 
				
			||||||
        secret: '*:*.some-random-string',
 | 
					        secret: '*:*.some-random-string',
 | 
				
			||||||
        type: ApiTokenType.ADMIN,
 | 
					        type: ApiTokenType.ADMIN,
 | 
				
			||||||
        tokenName: 'admin',
 | 
					        tokenName: 'admin',
 | 
				
			||||||
@ -506,7 +514,7 @@ test('create config should be idempotent in terms of tokens', async () => {
 | 
				
			|||||||
    process.env.INIT_FRONTEND_API_TOKENS = 'frontend:development.some-token1';
 | 
					    process.env.INIT_FRONTEND_API_TOKENS = 'frontend:development.some-token1';
 | 
				
			||||||
    const token = {
 | 
					    const token = {
 | 
				
			||||||
        environment: '*',
 | 
					        environment: '*',
 | 
				
			||||||
        project: '*',
 | 
					        projects: ['*'],
 | 
				
			||||||
        secret: '*:*.some-random-string',
 | 
					        secret: '*:*.some-random-string',
 | 
				
			||||||
        type: ApiTokenType.ADMIN,
 | 
					        type: ApiTokenType.ADMIN,
 | 
				
			||||||
        tokenName: 'admin',
 | 
					        tokenName: 'admin',
 | 
				
			||||||
 | 
				
			|||||||
@ -37,7 +37,7 @@ import {
 | 
				
			|||||||
    secondsToMilliseconds,
 | 
					    secondsToMilliseconds,
 | 
				
			||||||
} from 'date-fns';
 | 
					} from 'date-fns';
 | 
				
			||||||
import EventEmitter from 'events';
 | 
					import EventEmitter from 'events';
 | 
				
			||||||
import { mapLegacyToken, validateApiToken } from './types/models/api-token.js';
 | 
					import { validateApiToken } from './types/models/api-token.js';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    parseEnvVarBoolean,
 | 
					    parseEnvVarBoolean,
 | 
				
			||||||
    parseEnvVarJSON,
 | 
					    parseEnvVarJSON,
 | 
				
			||||||
@ -417,13 +417,13 @@ const loadTokensFromString = (
 | 
				
			|||||||
        const [environment = '*'] = rest.split('.');
 | 
					        const [environment = '*'] = rest.split('.');
 | 
				
			||||||
        const token = {
 | 
					        const token = {
 | 
				
			||||||
            createdAt: undefined,
 | 
					            createdAt: undefined,
 | 
				
			||||||
            project,
 | 
					            projects: [project],
 | 
				
			||||||
            environment,
 | 
					            environment,
 | 
				
			||||||
            secret,
 | 
					            secret,
 | 
				
			||||||
            type: tokenType,
 | 
					            type: tokenType,
 | 
				
			||||||
            tokenName: 'admin',
 | 
					            tokenName: 'admin',
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        validateApiToken(mapLegacyToken(token));
 | 
					        validateApiToken(token);
 | 
				
			||||||
        return token;
 | 
					        return token;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    return tokens;
 | 
					    return tokens;
 | 
				
			||||||
 | 
				
			|||||||
@ -50,7 +50,6 @@ const tokenRowReducer = (acc, tokenRow) => {
 | 
				
			|||||||
            createdAt: token.created_at,
 | 
					            createdAt: token.created_at,
 | 
				
			||||||
            alias: token.alias,
 | 
					            alias: token.alias,
 | 
				
			||||||
            seenAt: token.seen_at,
 | 
					            seenAt: token.seen_at,
 | 
				
			||||||
            username: token.token_name ? token.token_name : token.username,
 | 
					 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const currentToken = acc[tokenRow.secret];
 | 
					    const currentToken = acc[tokenRow.secret];
 | 
				
			||||||
@ -65,8 +64,8 @@ const tokenRowReducer = (acc, tokenRow) => {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const toRow = (newToken: IApiTokenCreate) => ({
 | 
					const toRow = (newToken: IApiTokenCreate) => ({
 | 
				
			||||||
    username: newToken.tokenName ?? newToken.username,
 | 
					    username: newToken.tokenName,
 | 
				
			||||||
    token_name: newToken.tokenName ?? newToken.username,
 | 
					    token_name: newToken.tokenName,
 | 
				
			||||||
    secret: newToken.secret,
 | 
					    secret: newToken.secret,
 | 
				
			||||||
    type: newToken.type,
 | 
					    type: newToken.type,
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
@ -192,7 +191,6 @@ export class ApiTokenStore implements IApiTokenStore {
 | 
				
			|||||||
            await Promise.all(updateProjectTasks);
 | 
					            await Promise.all(updateProjectTasks);
 | 
				
			||||||
            return {
 | 
					            return {
 | 
				
			||||||
                ...newToken,
 | 
					                ...newToken,
 | 
				
			||||||
                username: newToken.tokenName,
 | 
					 | 
				
			||||||
                alias: newToken.alias || null,
 | 
					                alias: newToken.alias || null,
 | 
				
			||||||
                project: newToken.projects?.join(',') || '*',
 | 
					                project: newToken.projects?.join(',') || '*',
 | 
				
			||||||
                createdAt: row.created_at,
 | 
					                createdAt: row.created_at,
 | 
				
			||||||
 | 
				
			|||||||
@ -2436,11 +2436,11 @@ test('should also delete api tokens that were only bound to deleted project', as
 | 
				
			|||||||
        auditUser,
 | 
					        auditUser,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const token = await apiTokenService.createApiToken({
 | 
					    const token = await apiTokenService.createApiTokenWithProjects({
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
        tokenName,
 | 
					        tokenName,
 | 
				
			||||||
        environment: DEFAULT_ENV,
 | 
					        environment: DEFAULT_ENV,
 | 
				
			||||||
        project: project,
 | 
					        projects: [project],
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await projectService.deleteProject(project, user, auditUser);
 | 
					    await projectService.deleteProject(project, user, auditUser);
 | 
				
			||||||
@ -2471,7 +2471,7 @@ test('should not delete project-bound api tokens still bound to project', async
 | 
				
			|||||||
        auditUser,
 | 
					        auditUser,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const token = await apiTokenService.createApiToken({
 | 
					    const token = await apiTokenService.createApiTokenWithProjects({
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
        tokenName,
 | 
					        tokenName,
 | 
				
			||||||
        environment: DEFAULT_ENV,
 | 
					        environment: DEFAULT_ENV,
 | 
				
			||||||
@ -2507,7 +2507,7 @@ test('should delete project-bound api tokens when all projects they belong to ar
 | 
				
			|||||||
        auditUser,
 | 
					        auditUser,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const token = await apiTokenService.createApiToken({
 | 
					    const token = await apiTokenService.createApiTokenWithProjects({
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
        tokenName,
 | 
					        tokenName,
 | 
				
			||||||
        environment: DEFAULT_ENV,
 | 
					        environment: DEFAULT_ENV,
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,6 @@ import type { ApiTokenSchema } from './api-token-schema.js';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const defaultData: ApiTokenSchema = {
 | 
					const defaultData: ApiTokenSchema = {
 | 
				
			||||||
    secret: '',
 | 
					    secret: '',
 | 
				
			||||||
    username: '',
 | 
					 | 
				
			||||||
    tokenName: '',
 | 
					    tokenName: '',
 | 
				
			||||||
    type: ApiTokenType.CLIENT,
 | 
					    type: ApiTokenType.CLIENT,
 | 
				
			||||||
    environment: '',
 | 
					    environment: '',
 | 
				
			||||||
 | 
				
			|||||||
@ -5,14 +5,7 @@ export const apiTokenSchema = {
 | 
				
			|||||||
    $id: '#/components/schemas/apiTokenSchema',
 | 
					    $id: '#/components/schemas/apiTokenSchema',
 | 
				
			||||||
    type: 'object',
 | 
					    type: 'object',
 | 
				
			||||||
    additionalProperties: false,
 | 
					    additionalProperties: false,
 | 
				
			||||||
    required: [
 | 
					    required: ['secret', 'tokenName', 'type', 'projects', 'createdAt'],
 | 
				
			||||||
        'secret',
 | 
					 | 
				
			||||||
        'tokenName',
 | 
					 | 
				
			||||||
        'type',
 | 
					 | 
				
			||||||
        'project',
 | 
					 | 
				
			||||||
        'projects',
 | 
					 | 
				
			||||||
        'createdAt',
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
        'An overview of an [Unleash API token](https://docs.getunleash.io/reference/api-tokens-and-client-keys).',
 | 
					        'An overview of an [Unleash API token](https://docs.getunleash.io/reference/api-tokens-and-client-keys).',
 | 
				
			||||||
    properties: {
 | 
					    properties: {
 | 
				
			||||||
@ -21,13 +14,6 @@ export const apiTokenSchema = {
 | 
				
			|||||||
            description: 'The token used for authentication.',
 | 
					            description: 'The token used for authentication.',
 | 
				
			||||||
            example: 'project:environment.xyzrandomstring',
 | 
					            example: 'project:environment.xyzrandomstring',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        username: {
 | 
					 | 
				
			||||||
            type: 'string',
 | 
					 | 
				
			||||||
            deprecated: true,
 | 
					 | 
				
			||||||
            description:
 | 
					 | 
				
			||||||
                'This property was deprecated in Unleash v5. Prefer the `tokenName` property instead.',
 | 
					 | 
				
			||||||
            example: 'a-name',
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        tokenName: {
 | 
					        tokenName: {
 | 
				
			||||||
            type: 'string',
 | 
					            type: 'string',
 | 
				
			||||||
            description: 'A unique name for this particular token',
 | 
					            description: 'A unique name for this particular token',
 | 
				
			||||||
 | 
				
			|||||||
@ -1,17 +1,5 @@
 | 
				
			|||||||
import type { FromSchema } from 'json-schema-to-ts';
 | 
					import type { FromSchema } from 'json-schema-to-ts';
 | 
				
			||||||
import { mergeAllOfs } from '../util/all-of.js';
 | 
					import { mergeAllOfs } from '../util/all-of.js';
 | 
				
			||||||
const adminSchema = {
 | 
					 | 
				
			||||||
    required: ['type'],
 | 
					 | 
				
			||||||
    type: 'object',
 | 
					 | 
				
			||||||
    properties: {
 | 
					 | 
				
			||||||
        type: {
 | 
					 | 
				
			||||||
            type: 'string',
 | 
					 | 
				
			||||||
            pattern: '^[Aa][Dd][Mm][Ii][Nn]$',
 | 
					 | 
				
			||||||
            description: `An admin token. Must be the string "admin" (not case sensitive).`,
 | 
					 | 
				
			||||||
            example: 'admin',
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
} as const;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const tokenNameSchema = {
 | 
					const tokenNameSchema = {
 | 
				
			||||||
    type: 'object',
 | 
					    type: 'object',
 | 
				
			||||||
@ -25,20 +13,6 @@ const tokenNameSchema = {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
} as const;
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const usernameSchema = {
 | 
					 | 
				
			||||||
    type: 'object',
 | 
					 | 
				
			||||||
    required: ['username'],
 | 
					 | 
				
			||||||
    properties: {
 | 
					 | 
				
			||||||
        username: {
 | 
					 | 
				
			||||||
            deprecated: true,
 | 
					 | 
				
			||||||
            type: 'string',
 | 
					 | 
				
			||||||
            description:
 | 
					 | 
				
			||||||
                'The name of the token. This property was deprecated in v5. Use `tokenName` instead.',
 | 
					 | 
				
			||||||
            example: 'token-64523',
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
} as const;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const clientFrontendSchema = {
 | 
					const clientFrontendSchema = {
 | 
				
			||||||
    required: ['type'],
 | 
					    required: ['type'],
 | 
				
			||||||
    type: 'object',
 | 
					    type: 'object',
 | 
				
			||||||
@ -100,12 +74,7 @@ export const createApiTokenSchema = {
 | 
				
			|||||||
    type: 'object',
 | 
					    type: 'object',
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
        'The data required to create an [Unleash API token](https://docs.getunleash.io/reference/api-tokens-and-client-keys).',
 | 
					        'The data required to create an [Unleash API token](https://docs.getunleash.io/reference/api-tokens-and-client-keys).',
 | 
				
			||||||
    oneOf: [
 | 
					    oneOf: [mergeAllOfs([expireSchema, clientFrontendSchema, tokenNameSchema])],
 | 
				
			||||||
        mergeAllOfs([expireSchema, adminSchema, tokenNameSchema]),
 | 
					 | 
				
			||||||
        mergeAllOfs([expireSchema, adminSchema, usernameSchema]),
 | 
					 | 
				
			||||||
        mergeAllOfs([expireSchema, clientFrontendSchema, tokenNameSchema]),
 | 
					 | 
				
			||||||
        mergeAllOfs([expireSchema, clientFrontendSchema, usernameSchema]),
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
    components: {},
 | 
					    components: {},
 | 
				
			||||||
} as const;
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										41
									
								
								src/lib/openapi/spec/create-project-api-token-schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/lib/openapi/spec/create-project-api-token-schema.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					import type { FromSchema } from 'json-schema-to-ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const createProjectApiTokenSchema = {
 | 
				
			||||||
 | 
					    type: 'object',
 | 
				
			||||||
 | 
					    required: ['tokenName', 'type'],
 | 
				
			||||||
 | 
					    $id: '#/components/schemas/createProjectApiTokenSchema',
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					        'The schema for creating a project API token. This schema is used to create a new project API token.',
 | 
				
			||||||
 | 
					    properties: {
 | 
				
			||||||
 | 
					        type: {
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					            pattern:
 | 
				
			||||||
 | 
					                '^([Cc][Ll][Ii][Ee][Nn][Tt]|[Ff][Rr][Oo][Nn][Tt][Ee][Nn][Dd])$',
 | 
				
			||||||
 | 
					            description: `A client or frontend token. Must be one of the strings "client" or "frontend" (not case sensitive).`,
 | 
				
			||||||
 | 
					            example: 'frontend',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        environment: {
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					            description:
 | 
				
			||||||
 | 
					                'The environment that the token should be valid for. Defaults to "default".',
 | 
				
			||||||
 | 
					            example: 'development',
 | 
				
			||||||
 | 
					            default: 'default',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        expiresAt: {
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					            description:
 | 
				
			||||||
 | 
					                'The date and time when the token should expire. The date should be in ISO 8601 format.',
 | 
				
			||||||
 | 
					            example: '2023-10-01T00:00:00Z',
 | 
				
			||||||
 | 
					            format: 'date-time',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        tokenName: {
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					            description: 'A unique name for this particular token',
 | 
				
			||||||
 | 
					            example: 'some-user',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    components: {},
 | 
				
			||||||
 | 
					} as const;
 | 
				
			||||||
 | 
					export type CreateProjectApiTokenSchema = FromSchema<
 | 
				
			||||||
 | 
					    typeof createProjectApiTokenSchema
 | 
				
			||||||
 | 
					>;
 | 
				
			||||||
@ -53,6 +53,7 @@ export * from './create-feature-strategy-schema.js';
 | 
				
			|||||||
export * from './create-group-schema.js';
 | 
					export * from './create-group-schema.js';
 | 
				
			||||||
export * from './create-invited-user-schema.js';
 | 
					export * from './create-invited-user-schema.js';
 | 
				
			||||||
export * from './create-pat-schema.js';
 | 
					export * from './create-pat-schema.js';
 | 
				
			||||||
 | 
					export * from './create-project-api-token-schema.js';
 | 
				
			||||||
export * from './create-strategy-schema.js';
 | 
					export * from './create-strategy-schema.js';
 | 
				
			||||||
export * from './create-strategy-variant-schema.js';
 | 
					export * from './create-strategy-variant-schema.js';
 | 
				
			||||||
export * from './create-tag-schema.js';
 | 
					export * from './create-tag-schema.js';
 | 
				
			||||||
 | 
				
			|||||||
@ -1,90 +0,0 @@
 | 
				
			|||||||
import permissions from '../../../test/fixtures/permissions.js';
 | 
					 | 
				
			||||||
import { createTestConfig } from '../../../test/config/test-config.js';
 | 
					 | 
				
			||||||
import createStores from '../../../test/fixtures/store.js';
 | 
					 | 
				
			||||||
import { createServices } from '../../services/index.js';
 | 
					 | 
				
			||||||
import getApp from '../../app.js';
 | 
					 | 
				
			||||||
import supertest from 'supertest';
 | 
					 | 
				
			||||||
import { addDays } from 'date-fns';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function getSetup() {
 | 
					 | 
				
			||||||
    const base = `/random${Math.round(Math.random() * 1000)}`;
 | 
					 | 
				
			||||||
    const perms = permissions();
 | 
					 | 
				
			||||||
    const config = createTestConfig({
 | 
					 | 
				
			||||||
        preHook: perms.hook,
 | 
					 | 
				
			||||||
        server: { baseUriPath: base },
 | 
					 | 
				
			||||||
        //@ts-ignore - Just testing, so only need the isEnabled call here
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    const stores = createStores();
 | 
					 | 
				
			||||||
    const services = createServices(stores, config);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    //@ts-expect-error: we're accessing a private field, but we need
 | 
					 | 
				
			||||||
    //to set up an environment to test the functionality. Because we
 | 
					 | 
				
			||||||
    //don't have a db to use, we need to access the service's store
 | 
					 | 
				
			||||||
    //directly.
 | 
					 | 
				
			||||||
    await services.apiTokenService.environmentStore.create({
 | 
					 | 
				
			||||||
        name: 'development',
 | 
					 | 
				
			||||||
        type: 'development',
 | 
					 | 
				
			||||||
        enabled: true,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const app = await getApp(config, stores, services);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
        base,
 | 
					 | 
				
			||||||
        request: supertest(app),
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
describe('Admin token killswitch', () => {
 | 
					 | 
				
			||||||
    test('If killswitch is on we will get an operation denied if we try to create an admin token', async () => {
 | 
					 | 
				
			||||||
        const setup = await getSetup();
 | 
					 | 
				
			||||||
        return setup.request
 | 
					 | 
				
			||||||
            .post(`${setup.base}/api/admin/api-tokens`)
 | 
					 | 
				
			||||||
            .set('Content-Type', 'application/json')
 | 
					 | 
				
			||||||
            .send({
 | 
					 | 
				
			||||||
                expiresAt: addDays(new Date(), 60),
 | 
					 | 
				
			||||||
                type: 'ADMIN',
 | 
					 | 
				
			||||||
                tokenName: 'Killswitched',
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .expect(403)
 | 
					 | 
				
			||||||
            .expect((res) => {
 | 
					 | 
				
			||||||
                expect(res.body.message).toBe(
 | 
					 | 
				
			||||||
                    'Admin tokens are disabled in this instance. Use a Service account or a PAT to access admin operations instead',
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    test('If killswitch is on we can still create a client token', async () => {
 | 
					 | 
				
			||||||
        const setup = await getSetup();
 | 
					 | 
				
			||||||
        return setup.request
 | 
					 | 
				
			||||||
            .post(`${setup.base}/api/admin/api-tokens`)
 | 
					 | 
				
			||||||
            .set('Content-Type', 'application/json')
 | 
					 | 
				
			||||||
            .send({
 | 
					 | 
				
			||||||
                expiresAt: addDays(new Date(), 60),
 | 
					 | 
				
			||||||
                type: 'CLIENT',
 | 
					 | 
				
			||||||
                environment: 'development',
 | 
					 | 
				
			||||||
                projects: ['*'],
 | 
					 | 
				
			||||||
                tokenName: 'Client',
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .expect(201)
 | 
					 | 
				
			||||||
            .expect((res) => {
 | 
					 | 
				
			||||||
                expect(res.body.secret).toBeTruthy();
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    test('If killswitch is on we can still create a frontend token', async () => {
 | 
					 | 
				
			||||||
        const setup = await getSetup();
 | 
					 | 
				
			||||||
        return setup.request
 | 
					 | 
				
			||||||
            .post(`${setup.base}/api/admin/api-tokens`)
 | 
					 | 
				
			||||||
            .set('Content-Type', 'application/json')
 | 
					 | 
				
			||||||
            .send({
 | 
					 | 
				
			||||||
                expiresAt: addDays(new Date(), 60),
 | 
					 | 
				
			||||||
                type: 'FRONTEND',
 | 
					 | 
				
			||||||
                environment: 'development',
 | 
					 | 
				
			||||||
                projects: ['*'],
 | 
					 | 
				
			||||||
                tokenName: 'Frontend',
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .expect(201)
 | 
					 | 
				
			||||||
            .expect((res) => {
 | 
					 | 
				
			||||||
                expect(res.body.secret).toBeTruthy();
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
@ -43,6 +43,7 @@ import {
 | 
				
			|||||||
} from '../../openapi/util/standard-responses.js';
 | 
					} from '../../openapi/util/standard-responses.js';
 | 
				
			||||||
import type { FrontendApiService } from '../../features/frontend-api/frontend-api-service.js';
 | 
					import type { FrontendApiService } from '../../features/frontend-api/frontend-api-service.js';
 | 
				
			||||||
import { OperationDeniedError } from '../../error/index.js';
 | 
					import { OperationDeniedError } from '../../error/index.js';
 | 
				
			||||||
 | 
					import type { CreateApiTokenSchema } from '../../internals.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface TokenParam {
 | 
					interface TokenParam {
 | 
				
			||||||
    token: string;
 | 
					    token: string;
 | 
				
			||||||
@ -299,24 +300,20 @@ export class ApiTokenController extends Controller {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async createApiToken(
 | 
					    async createApiToken(
 | 
				
			||||||
        req: IAuthRequest,
 | 
					        req: IAuthRequest<CreateApiTokenSchema>,
 | 
				
			||||||
        res: Response<ApiTokenSchema>,
 | 
					        res: Response<ApiTokenSchema>,
 | 
				
			||||||
    ): Promise<any> {
 | 
					    ): Promise<any> {
 | 
				
			||||||
        const createToken = await createApiToken.validateAsync(req.body);
 | 
					        const createToken = await createApiToken.validateAsync(req.body);
 | 
				
			||||||
        const permissionRequired = tokenTypeToCreatePermission(
 | 
					        const permissionRequired = tokenTypeToCreatePermission(
 | 
				
			||||||
            createToken.type,
 | 
					            createToken.type,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        if (createToken.type.toUpperCase() === 'ADMIN') {
 | 
					
 | 
				
			||||||
            throw new OperationDeniedError(
 | 
					 | 
				
			||||||
                `Admin tokens are disabled in this instance. Use a Service account or a PAT to access admin operations instead`,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        const hasPermission = await this.accessService.hasPermission(
 | 
					        const hasPermission = await this.accessService.hasPermission(
 | 
				
			||||||
            req.user,
 | 
					            req.user,
 | 
				
			||||||
            permissionRequired,
 | 
					            permissionRequired,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        if (hasPermission) {
 | 
					        if (hasPermission) {
 | 
				
			||||||
            const token = await this.apiTokenService.createApiToken(
 | 
					            const token = await this.apiTokenService.createApiTokenWithProjects(
 | 
				
			||||||
                createToken,
 | 
					                createToken,
 | 
				
			||||||
                req.audit,
 | 
					                req.audit,
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
				
			|||||||
@ -32,8 +32,9 @@ import Controller from '../../controller.js';
 | 
				
			|||||||
import type { Logger } from '../../../logger.js';
 | 
					import type { Logger } from '../../../logger.js';
 | 
				
			||||||
import type { Response } from 'express';
 | 
					import type { Response } from 'express';
 | 
				
			||||||
import { timingSafeEqual } from 'crypto';
 | 
					import { timingSafeEqual } from 'crypto';
 | 
				
			||||||
import { createApiToken } from '../../../schema/api-token-schema.js';
 | 
					 | 
				
			||||||
import { OperationDeniedError } from '../../../error/index.js';
 | 
					import { OperationDeniedError } from '../../../error/index.js';
 | 
				
			||||||
 | 
					import type { CreateProjectApiTokenSchema } from '../../../openapi/spec/create-project-api-token-schema.js';
 | 
				
			||||||
 | 
					import { createProjectApiToken } from '../../../schema/create-project-api-token-schema.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ProjectTokenParam {
 | 
					interface ProjectTokenParam {
 | 
				
			||||||
    token: string;
 | 
					    token: string;
 | 
				
			||||||
@ -109,7 +110,9 @@ export class ProjectApiTokenController extends Controller {
 | 
				
			|||||||
                openApiService.validPath({
 | 
					                openApiService.validPath({
 | 
				
			||||||
                    tags: ['Projects'],
 | 
					                    tags: ['Projects'],
 | 
				
			||||||
                    operationId: 'createProjectApiToken',
 | 
					                    operationId: 'createProjectApiToken',
 | 
				
			||||||
                    requestBody: createRequestSchema('createApiTokenSchema'),
 | 
					                    requestBody: createRequestSchema(
 | 
				
			||||||
 | 
					                        'createProjectApiTokenSchema',
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                    summary: 'Create a project API token.',
 | 
					                    summary: 'Create a project API token.',
 | 
				
			||||||
                    description:
 | 
					                    description:
 | 
				
			||||||
                        'Endpoint that allows creation of [project API tokens](https://docs.getunleash.io/reference/api-tokens-and-client-keys#api-token-visibility) for the specified project.',
 | 
					                        'Endpoint that allows creation of [project API tokens](https://docs.getunleash.io/reference/api-tokens-and-client-keys#api-token-visibility) for the specified project.',
 | 
				
			||||||
@ -160,10 +163,10 @@ export class ProjectApiTokenController extends Controller {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async createProjectApiToken(
 | 
					    async createProjectApiToken(
 | 
				
			||||||
        req: IAuthRequest,
 | 
					        req: IAuthRequest<{ projectId: string }, CreateProjectApiTokenSchema>,
 | 
				
			||||||
        res: Response<ApiTokenSchema>,
 | 
					        res: Response<ApiTokenSchema>,
 | 
				
			||||||
    ): Promise<any> {
 | 
					    ): Promise<any> {
 | 
				
			||||||
        const createToken = await createApiToken.validateAsync(req.body);
 | 
					        const createToken = await createProjectApiToken.validateAsync(req.body);
 | 
				
			||||||
        const { projectId } = req.params;
 | 
					        const { projectId } = req.params;
 | 
				
			||||||
        await this.projectService.getProject(projectId); // Validates that the project exists
 | 
					        await this.projectService.getProject(projectId); // Validates that the project exists
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -178,30 +181,17 @@ export class ProjectApiTokenController extends Controller {
 | 
				
			|||||||
                `You don't have the necessary access [${permissionRequired}] to perform this operation]`,
 | 
					                `You don't have the necessary access [${permissionRequired}] to perform this operation]`,
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (!createToken.project) {
 | 
					        const token = await this.apiTokenService.createApiTokenWithProjects(
 | 
				
			||||||
            createToken.project = projectId;
 | 
					            { ...createToken, projects: [projectId] },
 | 
				
			||||||
        }
 | 
					            req.audit,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
        if (
 | 
					        this.openApiService.respondWithValidation(
 | 
				
			||||||
            createToken.projects.length === 1 &&
 | 
					            201,
 | 
				
			||||||
            createToken.projects[0] === projectId
 | 
					            res,
 | 
				
			||||||
        ) {
 | 
					            apiTokenSchema.$id,
 | 
				
			||||||
            const token = await this.apiTokenService.createApiToken(
 | 
					            serializeDates(token),
 | 
				
			||||||
                createToken,
 | 
					            { location: `api-tokens` },
 | 
				
			||||||
                req.audit,
 | 
					        );
 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            this.openApiService.respondWithValidation(
 | 
					 | 
				
			||||||
                201,
 | 
					 | 
				
			||||||
                res,
 | 
					 | 
				
			||||||
                apiTokenSchema.$id,
 | 
					 | 
				
			||||||
                serializeDates(token),
 | 
					 | 
				
			||||||
                { location: `api-tokens` },
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            res.statusMessage =
 | 
					 | 
				
			||||||
                'Project level tokens can only be created for one project';
 | 
					 | 
				
			||||||
            res.status(400);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async deleteProjectApiToken(
 | 
					    async deleteProjectApiToken(
 | 
				
			||||||
 | 
				
			|||||||
@ -1,51 +1,50 @@
 | 
				
			|||||||
import { ALL } from '../types/models/api-token.js';
 | 
					import { ALL } from '../types/models/api-token.js';
 | 
				
			||||||
import { createApiToken } from './api-token-schema.js';
 | 
					import { createApiToken } from './api-token-schema.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('should reject token with projects and project', async () => {
 | 
					test('should ignore token extra project field', async () => {
 | 
				
			||||||
    expect.assertions(1);
 | 
					    expect.assertions(0);
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
        await createApiToken.validateAsync({
 | 
					        await createApiToken.validateAsync({
 | 
				
			||||||
            username: 'test',
 | 
					            tokenName: 'test',
 | 
				
			||||||
            type: 'admin',
 | 
					            type: 'client',
 | 
				
			||||||
            project: 'default',
 | 
					            project: 'default',
 | 
				
			||||||
            projects: ['default'],
 | 
					            projects: ['default'],
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
        expect(error.details[0].message).toEqual(
 | 
					        expect(error).toBeUndefined();
 | 
				
			||||||
            '"project" must not exist simultaneously with [projects]',
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('should not have default project set if projects is present', async () => {
 | 
					test('should not have default project set if projects is present', async () => {
 | 
				
			||||||
    const token = await createApiToken.validateAsync({
 | 
					    const token = await createApiToken.validateAsync({
 | 
				
			||||||
        username: 'test',
 | 
					        tokenName: 'test',
 | 
				
			||||||
        type: 'admin',
 | 
					        type: 'client',
 | 
				
			||||||
        projects: ['default'],
 | 
					        projects: ['default'],
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    expect(token.project).not.toBeDefined();
 | 
					    expect(token.project).not.toBeDefined();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('should have project set to default if projects is missing', async () => {
 | 
					test('should have a projects entry consisting of ALL if projects is missing', async () => {
 | 
				
			||||||
    const token = await createApiToken.validateAsync({
 | 
					    const token = await createApiToken.validateAsync({
 | 
				
			||||||
        username: 'test',
 | 
					        tokenName: 'test',
 | 
				
			||||||
        type: 'admin',
 | 
					        type: 'client',
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    expect(token.project).toBe(ALL);
 | 
					    expect(token.projects).toMatchObject([ALL]);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('should not have projects set if project is present', async () => {
 | 
					test('should not have project set after validation if project is present', async () => {
 | 
				
			||||||
    const token = await createApiToken.validateAsync({
 | 
					    const token = await createApiToken.validateAsync({
 | 
				
			||||||
        username: 'test',
 | 
					        tokenName: 'test',
 | 
				
			||||||
        type: 'admin',
 | 
					        type: 'client',
 | 
				
			||||||
        project: 'default',
 | 
					        project: 'default',
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    expect(token.projects).not.toBeDefined();
 | 
					    expect(token.project).not.toBeDefined();
 | 
				
			||||||
 | 
					    expect(token.projects).toMatchObject([ALL]);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('should allow for embedded proxy (frontend) key', async () => {
 | 
					test('should allow for embedded proxy (frontend) key', async () => {
 | 
				
			||||||
    const token = await createApiToken.validateAsync({
 | 
					    const token = await createApiToken.validateAsync({
 | 
				
			||||||
        username: 'test',
 | 
					        tokenName: 'test',
 | 
				
			||||||
        type: 'frontend',
 | 
					        type: 'frontend',
 | 
				
			||||||
        project: 'default',
 | 
					        project: 'default',
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -54,9 +53,8 @@ test('should allow for embedded proxy (frontend) key', async () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
test('should set environment to default for frontend key', async () => {
 | 
					test('should set environment to default for frontend key', async () => {
 | 
				
			||||||
    const token = await createApiToken.validateAsync({
 | 
					    const token = await createApiToken.validateAsync({
 | 
				
			||||||
        username: 'test',
 | 
					        tokenName: 'test',
 | 
				
			||||||
        type: 'frontend',
 | 
					        type: 'frontend',
 | 
				
			||||||
        project: 'default',
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    expect(token.environment).toEqual('default');
 | 
					    expect(token.environment).toEqual('default');
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -6,29 +6,18 @@ import { DEFAULT_ENV } from '../util/constants.js';
 | 
				
			|||||||
export const createApiToken = joi
 | 
					export const createApiToken = joi
 | 
				
			||||||
    .object()
 | 
					    .object()
 | 
				
			||||||
    .keys({
 | 
					    .keys({
 | 
				
			||||||
        username: joi.string().optional(),
 | 
					        tokenName: joi.string().required(),
 | 
				
			||||||
        tokenName: joi.string().optional(),
 | 
					 | 
				
			||||||
        type: joi
 | 
					        type: joi
 | 
				
			||||||
            .string()
 | 
					            .string()
 | 
				
			||||||
            .lowercase()
 | 
					            .lowercase()
 | 
				
			||||||
            .required()
 | 
					            .required()
 | 
				
			||||||
            .valid(
 | 
					            .valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND),
 | 
				
			||||||
                ApiTokenType.ADMIN,
 | 
					 | 
				
			||||||
                ApiTokenType.CLIENT,
 | 
					 | 
				
			||||||
                ApiTokenType.FRONTEND,
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        expiresAt: joi.date().optional(),
 | 
					        expiresAt: joi.date().optional(),
 | 
				
			||||||
        project: joi.when('projects', {
 | 
					        projects: joi.array().min(1).optional().default([ALL]),
 | 
				
			||||||
            not: joi.required(),
 | 
					 | 
				
			||||||
            then: joi.string().optional().default(ALL),
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
        projects: joi.array().min(0).optional(),
 | 
					 | 
				
			||||||
        environment: joi.when('type', {
 | 
					        environment: joi.when('type', {
 | 
				
			||||||
            is: joi.string().valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND),
 | 
					            is: joi.string().valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND),
 | 
				
			||||||
            then: joi.string().optional().default(DEFAULT_ENV),
 | 
					            then: joi.string().optional().default(DEFAULT_ENV),
 | 
				
			||||||
            otherwise: joi.string().optional().default(ALL),
 | 
					            otherwise: joi.string().optional().default(ALL),
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .nand('username', 'tokenName')
 | 
					 | 
				
			||||||
    .nand('project', 'projects')
 | 
					 | 
				
			||||||
    .options({ stripUnknown: true, allowUnknown: false, abortEarly: false });
 | 
					    .options({ stripUnknown: true, allowUnknown: false, abortEarly: false });
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										20
									
								
								src/lib/schema/create-project-api-token-schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/lib/schema/create-project-api-token-schema.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					import joi from 'joi';
 | 
				
			||||||
 | 
					import { ApiTokenType } from '../types/model.js';
 | 
				
			||||||
 | 
					import { DEFAULT_ENV } from '../util/constants.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const createProjectApiToken = joi
 | 
				
			||||||
 | 
					    .object()
 | 
				
			||||||
 | 
					    .keys({
 | 
				
			||||||
 | 
					        tokenName: joi.string().required(),
 | 
				
			||||||
 | 
					        type: joi
 | 
				
			||||||
 | 
					            .string()
 | 
				
			||||||
 | 
					            .lowercase()
 | 
				
			||||||
 | 
					            .required()
 | 
				
			||||||
 | 
					            .valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND),
 | 
				
			||||||
 | 
					        expiresAt: joi.date().optional(),
 | 
				
			||||||
 | 
					        environment: joi.when('type', {
 | 
				
			||||||
 | 
					            is: joi.string().valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND),
 | 
				
			||||||
 | 
					            then: joi.string().optional().default(DEFAULT_ENV),
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .options({ stripUnknown: true, allowUnknown: false, abortEarly: false });
 | 
				
			||||||
@ -19,7 +19,7 @@ import { vi } from 'vitest';
 | 
				
			|||||||
test('Should init api token', async () => {
 | 
					test('Should init api token', async () => {
 | 
				
			||||||
    const token = {
 | 
					    const token = {
 | 
				
			||||||
        environment: '*',
 | 
					        environment: '*',
 | 
				
			||||||
        project: '*',
 | 
					        projects: ['*'],
 | 
				
			||||||
        secret: '*:*:some-random-string',
 | 
					        secret: '*:*:some-random-string',
 | 
				
			||||||
        type: ApiTokenType.ADMIN,
 | 
					        type: ApiTokenType.ADMIN,
 | 
				
			||||||
        tokenName: 'admin',
 | 
					        tokenName: 'admin',
 | 
				
			||||||
 | 
				
			|||||||
@ -5,11 +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 {
 | 
				
			||||||
    type ILegacyApiTokenCreate,
 | 
					    resolveValidProjects,
 | 
				
			||||||
    validateApiToken,
 | 
					    validateApiToken,
 | 
				
			||||||
    validateApiTokenEnvironment,
 | 
					    validateApiTokenEnvironment,
 | 
				
			||||||
    mapLegacyToken,
 | 
					 | 
				
			||||||
    mapLegacyTokenWithSecret,
 | 
					 | 
				
			||||||
} 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';
 | 
				
			||||||
@ -194,7 +192,7 @@ export class ApiTokenService {
 | 
				
			|||||||
        return this.store.getAll();
 | 
					        return this.store.getAll();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async initApiTokens(tokens: ILegacyApiTokenCreate[]) {
 | 
					    async initApiTokens(tokens: IApiTokenCreate[]) {
 | 
				
			||||||
        const tokenCount = await this.store.count();
 | 
					        const tokenCount = await this.store.count();
 | 
				
			||||||
        if (tokenCount > 0) {
 | 
					        if (tokenCount > 0) {
 | 
				
			||||||
            this.logger.debug(
 | 
					            this.logger.debug(
 | 
				
			||||||
@ -203,9 +201,9 @@ export class ApiTokenService {
 | 
				
			|||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            const createAll = tokens
 | 
					            const createAll = tokens.map((t) =>
 | 
				
			||||||
                .map(mapLegacyTokenWithSecret)
 | 
					                this.insertNewApiToken(t, SYSTEM_USER_AUDIT),
 | 
				
			||||||
                .map((t) => this.insertNewApiToken(t, SYSTEM_USER_AUDIT));
 | 
					            );
 | 
				
			||||||
            await Promise.all(createAll);
 | 
					            await Promise.all(createAll);
 | 
				
			||||||
            this.logger.info(
 | 
					            this.logger.info(
 | 
				
			||||||
                `Created initial API tokens: ${tokens.map((t) => `(name: ${t.tokenName}, type: ${t.type})`).join(', ')}`,
 | 
					                `Created initial API tokens: ${tokens.map((t) => `(name: ${t.tokenName}, type: ${t.type})`).join(', ')}`,
 | 
				
			||||||
@ -273,17 +271,6 @@ export class ApiTokenService {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @deprecated This may be removed in a future release, prefer createApiTokenWithProjects
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public async createApiToken(
 | 
					 | 
				
			||||||
        newToken: Omit<ILegacyApiTokenCreate, 'secret'>,
 | 
					 | 
				
			||||||
        auditUser: IAuditUser = SYSTEM_USER_AUDIT,
 | 
					 | 
				
			||||||
    ): Promise<IApiToken> {
 | 
					 | 
				
			||||||
        const token = mapLegacyToken(newToken);
 | 
					 | 
				
			||||||
        return this.internalCreateApiTokenWithProjects(token, auditUser);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * @param newToken
 | 
					     * @param newToken
 | 
				
			||||||
     * @param createdBy should be IApiUser or IUser. Still supports optional or string for backward compatibility
 | 
					     * @param createdBy should be IApiUser or IUser. Still supports optional or string for backward compatibility
 | 
				
			||||||
@ -293,7 +280,13 @@ export class ApiTokenService {
 | 
				
			|||||||
        newToken: Omit<IApiTokenCreate, 'secret'>,
 | 
					        newToken: Omit<IApiTokenCreate, 'secret'>,
 | 
				
			||||||
        auditUser: IAuditUser = SYSTEM_USER_AUDIT,
 | 
					        auditUser: IAuditUser = SYSTEM_USER_AUDIT,
 | 
				
			||||||
    ): Promise<IApiToken> {
 | 
					    ): Promise<IApiToken> {
 | 
				
			||||||
        return this.internalCreateApiTokenWithProjects(newToken, auditUser);
 | 
					        return this.internalCreateApiTokenWithProjects(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                ...newToken,
 | 
				
			||||||
 | 
					                projects: resolveValidProjects(newToken.projects),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            auditUser,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private async internalCreateApiTokenWithProjects(
 | 
					    private async internalCreateApiTokenWithProjects(
 | 
				
			||||||
 | 
				
			|||||||
@ -210,10 +210,6 @@ export interface IApiTokenCreate {
 | 
				
			|||||||
    environment: string;
 | 
					    environment: string;
 | 
				
			||||||
    projects: string[];
 | 
					    projects: string[];
 | 
				
			||||||
    expiresAt?: Date;
 | 
					    expiresAt?: Date;
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @deprecated Use tokenName instead
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    username?: string;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IApiToken extends Omit<IApiTokenCreate, 'alias'> {
 | 
					export interface IApiToken extends Omit<IApiTokenCreate, 'alias'> {
 | 
				
			||||||
 | 
				
			|||||||
@ -4,64 +4,16 @@ import { ApiTokenType } from '../model.js';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const ALL = '*';
 | 
					export const ALL = '*';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ILegacyApiTokenCreate {
 | 
					 | 
				
			||||||
    secret: string;
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @deprecated Use tokenName instead
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    username?: string;
 | 
					 | 
				
			||||||
    type: ApiTokenType;
 | 
					 | 
				
			||||||
    environment?: string;
 | 
					 | 
				
			||||||
    project?: string;
 | 
					 | 
				
			||||||
    projects?: string[];
 | 
					 | 
				
			||||||
    expiresAt?: Date;
 | 
					 | 
				
			||||||
    tokenName?: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const isAllProjects = (projects: string[]): boolean => {
 | 
					export const isAllProjects = (projects: string[]): boolean => {
 | 
				
			||||||
    return projects && projects.length === 1 && projects[0] === ALL;
 | 
					    return projects && projects.length === 1 && projects[0] === ALL;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const mapLegacyProjects = (
 | 
					export const resolveValidProjects = (projects: string[]): string[] => {
 | 
				
			||||||
    project?: string,
 | 
					    if (projects.includes('*')) {
 | 
				
			||||||
    projects?: string[],
 | 
					        return ['*'];
 | 
				
			||||||
): string[] => {
 | 
					 | 
				
			||||||
    let cleanedProjects: string[];
 | 
					 | 
				
			||||||
    if (project) {
 | 
					 | 
				
			||||||
        cleanedProjects = [project];
 | 
					 | 
				
			||||||
    } else if (projects) {
 | 
					 | 
				
			||||||
        cleanedProjects = projects;
 | 
					 | 
				
			||||||
        if (cleanedProjects.includes('*')) {
 | 
					 | 
				
			||||||
            cleanedProjects = ['*'];
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        throw new BadDataError(
 | 
					 | 
				
			||||||
            'API tokens must either contain a project or projects field',
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return cleanedProjects;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const mapLegacyToken = (
 | 
					    return projects;
 | 
				
			||||||
    token: Omit<ILegacyApiTokenCreate, 'secret'>,
 | 
					 | 
				
			||||||
): Omit<IApiTokenCreate, 'secret'> => {
 | 
					 | 
				
			||||||
    const cleanedProjects = mapLegacyProjects(token.project, token.projects);
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
        tokenName: token.username ?? token.tokenName!,
 | 
					 | 
				
			||||||
        type: token.type,
 | 
					 | 
				
			||||||
        environment: token.environment || 'development',
 | 
					 | 
				
			||||||
        projects: cleanedProjects,
 | 
					 | 
				
			||||||
        expiresAt: token.expiresAt,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const mapLegacyTokenWithSecret = (
 | 
					 | 
				
			||||||
    token: ILegacyApiTokenCreate,
 | 
					 | 
				
			||||||
): IApiTokenCreate => {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
        ...mapLegacyToken(token),
 | 
					 | 
				
			||||||
        secret: token.secret,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const validateApiToken = ({
 | 
					export const validateApiToken = ({
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import type { Express } from 'express';
 | 
					import type { Express } from 'express';
 | 
				
			||||||
import type EventEmitter from 'events';
 | 
					import type EventEmitter from 'events';
 | 
				
			||||||
import type { LogLevel, LogProvider } from '../logger.js';
 | 
					import type { LogLevel, LogProvider } from '../logger.js';
 | 
				
			||||||
import type { ILegacyApiTokenCreate } from './models/api-token.js';
 | 
					import type { IApiTokenCreate } from './model.js';
 | 
				
			||||||
import type {
 | 
					import type {
 | 
				
			||||||
    IExperimentalOptions,
 | 
					    IExperimentalOptions,
 | 
				
			||||||
    IFlagContext,
 | 
					    IFlagContext,
 | 
				
			||||||
@ -85,7 +85,7 @@ export interface IAuthOption {
 | 
				
			|||||||
    customAuthHandler?: CustomAuthHandler;
 | 
					    customAuthHandler?: CustomAuthHandler;
 | 
				
			||||||
    createAdminUser?: boolean;
 | 
					    createAdminUser?: boolean;
 | 
				
			||||||
    initialAdminUser?: UsernameAdminUser;
 | 
					    initialAdminUser?: UsernameAdminUser;
 | 
				
			||||||
    initApiTokens: ILegacyApiTokenCreate[];
 | 
					    initApiTokens: IApiTokenCreate[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IImportOption {
 | 
					export interface IImportOption {
 | 
				
			||||||
 | 
				
			|||||||
@ -63,7 +63,7 @@ process.nextTick(async () => {
 | 
				
			|||||||
                    initApiTokens: [
 | 
					                    initApiTokens: [
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            environment: '*',
 | 
					                            environment: '*',
 | 
				
			||||||
                            project: '*',
 | 
					                            projects: ['*'],
 | 
				
			||||||
                            secret: '*:*.964a287e1b728cb5f4f3e0120df92cb5',
 | 
					                            secret: '*:*.964a287e1b728cb5f4f3e0120df92cb5',
 | 
				
			||||||
                            type: ApiTokenType.ADMIN,
 | 
					                            type: ApiTokenType.ADMIN,
 | 
				
			||||||
                            tokenName: 'some-user',
 | 
					                            tokenName: 'some-user',
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,4 @@
 | 
				
			|||||||
import {
 | 
					import { setupAppWithCustomAuth } from '../../helpers/test-helper.js';
 | 
				
			||||||
    setupAppWithAuth,
 | 
					 | 
				
			||||||
    setupAppWithCustomAuth,
 | 
					 | 
				
			||||||
} from '../../helpers/test-helper.js';
 | 
					 | 
				
			||||||
import dbInit, { type ITestDb } from '../../helpers/database-init.js';
 | 
					import dbInit, { type ITestDb } from '../../helpers/database-init.js';
 | 
				
			||||||
import getLogger from '../../../fixtures/no-logger.js';
 | 
					import getLogger from '../../../fixtures/no-logger.js';
 | 
				
			||||||
import { ApiTokenType } from '../../../../lib/types/model.js';
 | 
					import { ApiTokenType } from '../../../../lib/types/model.js';
 | 
				
			||||||
@ -16,7 +13,6 @@ import {
 | 
				
			|||||||
    SYSTEM_USER,
 | 
					    SYSTEM_USER,
 | 
				
			||||||
    SYSTEM_USER_AUDIT,
 | 
					    SYSTEM_USER_AUDIT,
 | 
				
			||||||
    SYSTEM_USER_ID,
 | 
					    SYSTEM_USER_ID,
 | 
				
			||||||
    TEST_AUDIT_USER,
 | 
					 | 
				
			||||||
    UPDATE_CLIENT_API_TOKEN,
 | 
					    UPDATE_CLIENT_API_TOKEN,
 | 
				
			||||||
} from '../../../../lib/types/index.js';
 | 
					} from '../../../../lib/types/index.js';
 | 
				
			||||||
import { addDays } from 'date-fns';
 | 
					import { addDays } from 'date-fns';
 | 
				
			||||||
@ -79,8 +75,7 @@ test('editor users should only get client or frontend tokens', async () => {
 | 
				
			|||||||
    await stores.apiTokenStore.insert({
 | 
					    await stores.apiTokenStore.insert({
 | 
				
			||||||
        environment: '',
 | 
					        environment: '',
 | 
				
			||||||
        projects: [],
 | 
					        projects: [],
 | 
				
			||||||
        tokenName: '',
 | 
					        tokenName: 'test',
 | 
				
			||||||
        username: 'test',
 | 
					 | 
				
			||||||
        secret: '*:environment.1234',
 | 
					        secret: '*:environment.1234',
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -88,8 +83,7 @@ test('editor users should only get client or frontend tokens', async () => {
 | 
				
			|||||||
    await stores.apiTokenStore.insert({
 | 
					    await stores.apiTokenStore.insert({
 | 
				
			||||||
        environment: '',
 | 
					        environment: '',
 | 
				
			||||||
        projects: [],
 | 
					        projects: [],
 | 
				
			||||||
        tokenName: '',
 | 
					        tokenName: 'frontend',
 | 
				
			||||||
        username: 'frontend',
 | 
					 | 
				
			||||||
        secret: '*:environment.12345',
 | 
					        secret: '*:environment.12345',
 | 
				
			||||||
        type: ApiTokenType.FRONTEND,
 | 
					        type: ApiTokenType.FRONTEND,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -97,8 +91,7 @@ test('editor users should only get client or frontend tokens', async () => {
 | 
				
			|||||||
    await stores.apiTokenStore.insert({
 | 
					    await stores.apiTokenStore.insert({
 | 
				
			||||||
        environment: '',
 | 
					        environment: '',
 | 
				
			||||||
        projects: [],
 | 
					        projects: [],
 | 
				
			||||||
        tokenName: '',
 | 
					        tokenName: 'test',
 | 
				
			||||||
        username: 'test',
 | 
					 | 
				
			||||||
        secret: '*:*.sdfsdf2d',
 | 
					        secret: '*:*.sdfsdf2d',
 | 
				
			||||||
        type: ApiTokenType.ADMIN,
 | 
					        type: ApiTokenType.ADMIN,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -141,8 +134,7 @@ test('viewer users should not be allowed to fetch tokens', async () => {
 | 
				
			|||||||
    await stores.apiTokenStore.insert({
 | 
					    await stores.apiTokenStore.insert({
 | 
				
			||||||
        environment: '',
 | 
					        environment: '',
 | 
				
			||||||
        projects: [],
 | 
					        projects: [],
 | 
				
			||||||
        tokenName: '',
 | 
					        tokenName: 'test',
 | 
				
			||||||
        username: 'test',
 | 
					 | 
				
			||||||
        secret: '*:environment.1234',
 | 
					        secret: '*:environment.1234',
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -150,8 +142,7 @@ test('viewer users should not be allowed to fetch tokens', async () => {
 | 
				
			|||||||
    await stores.apiTokenStore.insert({
 | 
					    await stores.apiTokenStore.insert({
 | 
				
			||||||
        environment: '',
 | 
					        environment: '',
 | 
				
			||||||
        projects: [],
 | 
					        projects: [],
 | 
				
			||||||
        tokenName: '',
 | 
					        tokenName: 'test',
 | 
				
			||||||
        username: 'test',
 | 
					 | 
				
			||||||
        secret: '*:*.sdfsdf2d',
 | 
					        secret: '*:*.sdfsdf2d',
 | 
				
			||||||
        type: ApiTokenType.ADMIN,
 | 
					        type: ApiTokenType.ADMIN,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -164,102 +155,6 @@ test('viewer users should not be allowed to fetch tokens', async () => {
 | 
				
			|||||||
    await destroy();
 | 
					    await destroy();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('Only token-admins should be allowed to create token', async () => {
 | 
					 | 
				
			||||||
    expect.assertions(0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const preHook = (app, config, { userService, accessService }) => {
 | 
					 | 
				
			||||||
        app.use('/api/admin/', async (req, res, next) => {
 | 
					 | 
				
			||||||
            const role = await accessService.getPredefinedRole(RoleName.EDITOR);
 | 
					 | 
				
			||||||
            req.user = await userService.createUser({
 | 
					 | 
				
			||||||
                email: 'editor2@example.com',
 | 
					 | 
				
			||||||
                rootRole: role.id,
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            next();
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const { request, destroy } = await setupAppWithCustomAuth(
 | 
					 | 
				
			||||||
        stores,
 | 
					 | 
				
			||||||
        preHook,
 | 
					 | 
				
			||||||
        undefined,
 | 
					 | 
				
			||||||
        db.rawDatabase,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await request
 | 
					 | 
				
			||||||
        .post('/api/admin/api-tokens')
 | 
					 | 
				
			||||||
        .send({
 | 
					 | 
				
			||||||
            username: 'default-admin',
 | 
					 | 
				
			||||||
            type: 'admin',
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .set('Content-Type', 'application/json')
 | 
					 | 
				
			||||||
        .expect(403);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await destroy();
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
test('Token-admin should not be allowed to create token', async () => {
 | 
					 | 
				
			||||||
    expect.assertions(0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const preHook = (app, config, { userService, accessService }) => {
 | 
					 | 
				
			||||||
        app.use('/api/admin/', async (req, res, next) => {
 | 
					 | 
				
			||||||
            const role = await accessService.getPredefinedRole(RoleName.ADMIN);
 | 
					 | 
				
			||||||
            req.user = await userService.createUser({
 | 
					 | 
				
			||||||
                email: 'admin@example.com',
 | 
					 | 
				
			||||||
                rootRole: role.id,
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            next();
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const { request, destroy } = await setupAppWithCustomAuth(
 | 
					 | 
				
			||||||
        stores,
 | 
					 | 
				
			||||||
        preHook,
 | 
					 | 
				
			||||||
        undefined,
 | 
					 | 
				
			||||||
        db.rawDatabase,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await request
 | 
					 | 
				
			||||||
        .post('/api/admin/api-tokens')
 | 
					 | 
				
			||||||
        .send({
 | 
					 | 
				
			||||||
            username: 'default-admin',
 | 
					 | 
				
			||||||
            type: 'admin',
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .set('Content-Type', 'application/json')
 | 
					 | 
				
			||||||
        .expect(403);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await destroy();
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
test('An admin should be forbidden to create an admin token', async () => {
 | 
					 | 
				
			||||||
    const { request, destroy, services } = await setupAppWithAuth(
 | 
					 | 
				
			||||||
        stores,
 | 
					 | 
				
			||||||
        undefined,
 | 
					 | 
				
			||||||
        db.rawDatabase,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const { secret } =
 | 
					 | 
				
			||||||
        await services.apiTokenService.createApiTokenWithProjects(
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                tokenName: 'default-admin',
 | 
					 | 
				
			||||||
                type: ApiTokenType.ADMIN,
 | 
					 | 
				
			||||||
                projects: ['*'],
 | 
					 | 
				
			||||||
                environment: '*',
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            TEST_AUDIT_USER,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await request
 | 
					 | 
				
			||||||
        .post('/api/admin/api-tokens')
 | 
					 | 
				
			||||||
        .send({
 | 
					 | 
				
			||||||
            username: 'default-admin',
 | 
					 | 
				
			||||||
            type: 'admin',
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .set('Authorization', secret)
 | 
					 | 
				
			||||||
        .set('Content-Type', 'application/json')
 | 
					 | 
				
			||||||
        .expect(403);
 | 
					 | 
				
			||||||
    await destroy();
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', async () => {
 | 
					test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', async () => {
 | 
				
			||||||
    expect.assertions(0);
 | 
					    expect.assertions(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -312,7 +207,7 @@ test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', asyn
 | 
				
			|||||||
    await request
 | 
					    await request
 | 
				
			||||||
        .post('/api/admin/projects/default/api-tokens')
 | 
					        .post('/api/admin/projects/default/api-tokens')
 | 
				
			||||||
        .send({
 | 
					        .send({
 | 
				
			||||||
            username: 'client-token-maker',
 | 
					            tokenName: 'client-token-maker',
 | 
				
			||||||
            type: 'client',
 | 
					            type: 'client',
 | 
				
			||||||
            projects: ['default'],
 | 
					            projects: ['default'],
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
@ -374,7 +269,7 @@ describe('Fine grained API token permissions', () => {
 | 
				
			|||||||
            await request
 | 
					            await request
 | 
				
			||||||
                .post('/api/admin/api-tokens')
 | 
					                .post('/api/admin/api-tokens')
 | 
				
			||||||
                .send({
 | 
					                .send({
 | 
				
			||||||
                    username: 'default-client',
 | 
					                    tokenName: 'default-client',
 | 
				
			||||||
                    type: 'client',
 | 
					                    type: 'client',
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
                .set('Content-Type', 'application/json')
 | 
					                .set('Content-Type', 'application/json')
 | 
				
			||||||
@ -431,70 +326,13 @@ describe('Fine grained API token permissions', () => {
 | 
				
			|||||||
            await request
 | 
					            await request
 | 
				
			||||||
                .post('/api/admin/api-tokens')
 | 
					                .post('/api/admin/api-tokens')
 | 
				
			||||||
                .send({
 | 
					                .send({
 | 
				
			||||||
                    username: 'default-frontend',
 | 
					                    tokenName: 'default-frontend',
 | 
				
			||||||
                    type: 'frontend',
 | 
					                    type: 'frontend',
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
                .set('Content-Type', 'application/json')
 | 
					                .set('Content-Type', 'application/json')
 | 
				
			||||||
                .expect(403);
 | 
					                .expect(403);
 | 
				
			||||||
            await destroy();
 | 
					            await destroy();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        test('should NOT be allowed to create ADMIN tokens', async () => {
 | 
					 | 
				
			||||||
            const preHook = (
 | 
					 | 
				
			||||||
                app,
 | 
					 | 
				
			||||||
                config,
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    userService,
 | 
					 | 
				
			||||||
                    accessService,
 | 
					 | 
				
			||||||
                }: Pick<IUnleashServices, 'userService' | 'accessService'>,
 | 
					 | 
				
			||||||
            ) => {
 | 
					 | 
				
			||||||
                app.use('/api/admin/', async (req, res, next) => {
 | 
					 | 
				
			||||||
                    const role = await accessService.getPredefinedRole(
 | 
					 | 
				
			||||||
                        RoleName.VIEWER,
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                    const user = await userService.createUser({
 | 
					 | 
				
			||||||
                        email: 'mylittlepony_admin@example.com',
 | 
					 | 
				
			||||||
                        rootRole: role.id,
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
                    req.user = user;
 | 
					 | 
				
			||||||
                    const createClientApiTokenRole =
 | 
					 | 
				
			||||||
                        await accessService.createRole(
 | 
					 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                name: 'client_token_creator_cannot_create_admin',
 | 
					 | 
				
			||||||
                                description: 'Can create client tokens',
 | 
					 | 
				
			||||||
                                permissions: [],
 | 
					 | 
				
			||||||
                                type: 'root-custom',
 | 
					 | 
				
			||||||
                                createdByUserId: SYSTEM_USER_ID,
 | 
					 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                            SYSTEM_USER_AUDIT,
 | 
					 | 
				
			||||||
                        );
 | 
					 | 
				
			||||||
                    await accessService.addPermissionToRole(
 | 
					 | 
				
			||||||
                        role.id,
 | 
					 | 
				
			||||||
                        CREATE_CLIENT_API_TOKEN,
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                    await accessService.addUserToRole(
 | 
					 | 
				
			||||||
                        user.id,
 | 
					 | 
				
			||||||
                        createClientApiTokenRole.id,
 | 
					 | 
				
			||||||
                        'default',
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                    next();
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
            const { request, destroy } = await setupAppWithCustomAuth(
 | 
					 | 
				
			||||||
                stores,
 | 
					 | 
				
			||||||
                preHook,
 | 
					 | 
				
			||||||
                undefined,
 | 
					 | 
				
			||||||
                db.rawDatabase,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            await request
 | 
					 | 
				
			||||||
                .post('/api/admin/api-tokens')
 | 
					 | 
				
			||||||
                .send({
 | 
					 | 
				
			||||||
                    username: 'default-admin',
 | 
					 | 
				
			||||||
                    type: 'admin',
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .set('Content-Type', 'application/json')
 | 
					 | 
				
			||||||
                .expect(403);
 | 
					 | 
				
			||||||
            await destroy();
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    describe('Read operations', () => {
 | 
					    describe('Read operations', () => {
 | 
				
			||||||
        test('READ_FRONTEND_API_TOKEN should be able to see FRONTEND tokens', async () => {
 | 
					        test('READ_FRONTEND_API_TOKEN should be able to see FRONTEND tokens', async () => {
 | 
				
			||||||
@ -546,9 +384,8 @@ describe('Fine grained API token permissions', () => {
 | 
				
			|||||||
            await stores.apiTokenStore.insert({
 | 
					            await stores.apiTokenStore.insert({
 | 
				
			||||||
                environment: '',
 | 
					                environment: '',
 | 
				
			||||||
                projects: [],
 | 
					                projects: [],
 | 
				
			||||||
                tokenName: '',
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                username: 'client',
 | 
					                tokenName: 'client',
 | 
				
			||||||
                secret: '*:environment.client_secret',
 | 
					                secret: '*:environment.client_secret',
 | 
				
			||||||
                type: ApiTokenType.CLIENT,
 | 
					                type: ApiTokenType.CLIENT,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@ -556,16 +393,14 @@ describe('Fine grained API token permissions', () => {
 | 
				
			|||||||
            await stores.apiTokenStore.insert({
 | 
					            await stores.apiTokenStore.insert({
 | 
				
			||||||
                environment: '',
 | 
					                environment: '',
 | 
				
			||||||
                projects: [],
 | 
					                projects: [],
 | 
				
			||||||
                tokenName: '',
 | 
					                tokenName: 'admin',
 | 
				
			||||||
                username: 'admin',
 | 
					 | 
				
			||||||
                secret: '*:*.sdfsdf2admin_secret',
 | 
					                secret: '*:*.sdfsdf2admin_secret',
 | 
				
			||||||
                type: ApiTokenType.ADMIN,
 | 
					                type: ApiTokenType.ADMIN,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
            await stores.apiTokenStore.insert({
 | 
					            await stores.apiTokenStore.insert({
 | 
				
			||||||
                environment: '',
 | 
					                environment: '',
 | 
				
			||||||
                projects: [],
 | 
					                projects: [],
 | 
				
			||||||
                tokenName: '',
 | 
					                tokenName: 'frontender',
 | 
				
			||||||
                username: 'frontender',
 | 
					 | 
				
			||||||
                secret: '*:environment:sdfsdf2dfrontend_Secret',
 | 
					                secret: '*:environment:sdfsdf2dfrontend_Secret',
 | 
				
			||||||
                type: ApiTokenType.FRONTEND,
 | 
					                type: ApiTokenType.FRONTEND,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@ -631,8 +466,7 @@ describe('Fine grained API token permissions', () => {
 | 
				
			|||||||
            await stores.apiTokenStore.insert({
 | 
					            await stores.apiTokenStore.insert({
 | 
				
			||||||
                environment: '',
 | 
					                environment: '',
 | 
				
			||||||
                projects: [],
 | 
					                projects: [],
 | 
				
			||||||
                tokenName: '',
 | 
					                tokenName: 'client',
 | 
				
			||||||
                username: 'client',
 | 
					 | 
				
			||||||
                secret: '*:environment.client_secret_1234',
 | 
					                secret: '*:environment.client_secret_1234',
 | 
				
			||||||
                type: ApiTokenType.CLIENT,
 | 
					                type: ApiTokenType.CLIENT,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@ -640,16 +474,14 @@ describe('Fine grained API token permissions', () => {
 | 
				
			|||||||
            await stores.apiTokenStore.insert({
 | 
					            await stores.apiTokenStore.insert({
 | 
				
			||||||
                environment: '',
 | 
					                environment: '',
 | 
				
			||||||
                projects: [],
 | 
					                projects: [],
 | 
				
			||||||
                tokenName: '',
 | 
					                tokenName: 'admin',
 | 
				
			||||||
                username: 'admin',
 | 
					 | 
				
			||||||
                secret: '*:*.admin_secret_1234',
 | 
					                secret: '*:*.admin_secret_1234',
 | 
				
			||||||
                type: ApiTokenType.ADMIN,
 | 
					                type: ApiTokenType.ADMIN,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
            await stores.apiTokenStore.insert({
 | 
					            await stores.apiTokenStore.insert({
 | 
				
			||||||
                environment: '',
 | 
					                environment: '',
 | 
				
			||||||
                projects: [],
 | 
					                projects: [],
 | 
				
			||||||
                tokenName: '',
 | 
					                tokenName: 'frontender',
 | 
				
			||||||
                username: 'frontender',
 | 
					 | 
				
			||||||
                secret: '*:environment.frontend_secret_1234',
 | 
					                secret: '*:environment.frontend_secret_1234',
 | 
				
			||||||
                type: ApiTokenType.FRONTEND,
 | 
					                type: ApiTokenType.FRONTEND,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@ -693,8 +525,7 @@ describe('Fine grained API token permissions', () => {
 | 
				
			|||||||
            await stores.apiTokenStore.insert({
 | 
					            await stores.apiTokenStore.insert({
 | 
				
			||||||
                environment: '',
 | 
					                environment: '',
 | 
				
			||||||
                projects: [],
 | 
					                projects: [],
 | 
				
			||||||
                tokenName: '',
 | 
					                tokenName: 'client',
 | 
				
			||||||
                username: 'client',
 | 
					 | 
				
			||||||
                secret: '*:environment.client_secret_4321',
 | 
					                secret: '*:environment.client_secret_4321',
 | 
				
			||||||
                type: ApiTokenType.CLIENT,
 | 
					                type: ApiTokenType.CLIENT,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@ -702,16 +533,14 @@ describe('Fine grained API token permissions', () => {
 | 
				
			|||||||
            await stores.apiTokenStore.insert({
 | 
					            await stores.apiTokenStore.insert({
 | 
				
			||||||
                environment: '',
 | 
					                environment: '',
 | 
				
			||||||
                projects: [],
 | 
					                projects: [],
 | 
				
			||||||
                tokenName: '',
 | 
					                tokenName: 'admin',
 | 
				
			||||||
                username: 'admin',
 | 
					 | 
				
			||||||
                secret: '*:*.admin_secret_4321',
 | 
					                secret: '*:*.admin_secret_4321',
 | 
				
			||||||
                type: ApiTokenType.ADMIN,
 | 
					                type: ApiTokenType.ADMIN,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
            await stores.apiTokenStore.insert({
 | 
					            await stores.apiTokenStore.insert({
 | 
				
			||||||
                environment: '',
 | 
					                environment: '',
 | 
				
			||||||
                projects: [],
 | 
					                projects: [],
 | 
				
			||||||
                tokenName: '',
 | 
					                tokenName: 'frontender',
 | 
				
			||||||
                username: 'frontender',
 | 
					 | 
				
			||||||
                secret: '*:environment.frontend_secret_4321',
 | 
					                secret: '*:environment.frontend_secret_4321',
 | 
				
			||||||
                type: ApiTokenType.FRONTEND,
 | 
					                type: ApiTokenType.FRONTEND,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@ -754,24 +583,21 @@ describe('Fine grained API token permissions', () => {
 | 
				
			|||||||
            await stores.apiTokenStore.insert({
 | 
					            await stores.apiTokenStore.insert({
 | 
				
			||||||
                environment: '',
 | 
					                environment: '',
 | 
				
			||||||
                projects: [],
 | 
					                projects: [],
 | 
				
			||||||
                tokenName: '',
 | 
					                tokenName: 'client',
 | 
				
			||||||
                username: 'client',
 | 
					 | 
				
			||||||
                secret: '*:environment.client_secret_4321',
 | 
					                secret: '*:environment.client_secret_4321',
 | 
				
			||||||
                type: ApiTokenType.CLIENT,
 | 
					                type: ApiTokenType.CLIENT,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
            await stores.apiTokenStore.insert({
 | 
					            await stores.apiTokenStore.insert({
 | 
				
			||||||
                environment: '',
 | 
					                environment: '',
 | 
				
			||||||
                projects: [],
 | 
					                projects: [],
 | 
				
			||||||
                tokenName: '',
 | 
					                tokenName: 'admin',
 | 
				
			||||||
                username: 'admin',
 | 
					 | 
				
			||||||
                secret: '*:*.admin_secret_4321',
 | 
					                secret: '*:*.admin_secret_4321',
 | 
				
			||||||
                type: ApiTokenType.ADMIN,
 | 
					                type: ApiTokenType.ADMIN,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
            await stores.apiTokenStore.insert({
 | 
					            await stores.apiTokenStore.insert({
 | 
				
			||||||
                environment: '',
 | 
					                environment: '',
 | 
				
			||||||
                projects: [],
 | 
					                projects: [],
 | 
				
			||||||
                tokenName: '',
 | 
					                tokenName: 'frontender',
 | 
				
			||||||
                username: 'frontender',
 | 
					 | 
				
			||||||
                secret: '*:environment.frontend_secret_4321',
 | 
					                secret: '*:environment.frontend_secret_4321',
 | 
				
			||||||
                type: ApiTokenType.FRONTEND,
 | 
					                type: ApiTokenType.FRONTEND,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@ -842,8 +668,7 @@ describe('Fine grained API token permissions', () => {
 | 
				
			|||||||
                const token = await stores.apiTokenStore.insert({
 | 
					                const token = await stores.apiTokenStore.insert({
 | 
				
			||||||
                    environment: '',
 | 
					                    environment: '',
 | 
				
			||||||
                    projects: [],
 | 
					                    projects: [],
 | 
				
			||||||
                    tokenName: '',
 | 
					                    tokenName: 'cilent',
 | 
				
			||||||
                    username: 'cilent',
 | 
					 | 
				
			||||||
                    secret: '*:environment.update_client_token',
 | 
					                    secret: '*:environment.update_client_token',
 | 
				
			||||||
                    type: ApiTokenType.CLIENT,
 | 
					                    type: ApiTokenType.CLIENT,
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
@ -904,8 +729,7 @@ describe('Fine grained API token permissions', () => {
 | 
				
			|||||||
                const token = await stores.apiTokenStore.insert({
 | 
					                const token = await stores.apiTokenStore.insert({
 | 
				
			||||||
                    environment: '',
 | 
					                    environment: '',
 | 
				
			||||||
                    projects: [],
 | 
					                    projects: [],
 | 
				
			||||||
                    tokenName: '',
 | 
					                    tokenName: 'frontend',
 | 
				
			||||||
                    username: 'frontend',
 | 
					 | 
				
			||||||
                    secret: '*:environment.update_frontend_token',
 | 
					                    secret: '*:environment.update_frontend_token',
 | 
				
			||||||
                    type: ApiTokenType.FRONTEND,
 | 
					                    type: ApiTokenType.FRONTEND,
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
@ -966,9 +790,8 @@ describe('Fine grained API token permissions', () => {
 | 
				
			|||||||
                const token = await stores.apiTokenStore.insert({
 | 
					                const token = await stores.apiTokenStore.insert({
 | 
				
			||||||
                    environment: '',
 | 
					                    environment: '',
 | 
				
			||||||
                    projects: [],
 | 
					                    projects: [],
 | 
				
			||||||
                    tokenName: '',
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    username: 'admin',
 | 
					                    tokenName: 'admin',
 | 
				
			||||||
                    secret: '*:*.update_admin_token',
 | 
					                    secret: '*:*.update_admin_token',
 | 
				
			||||||
                    type: ApiTokenType.ADMIN,
 | 
					                    type: ApiTokenType.ADMIN,
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
@ -1032,8 +855,7 @@ describe('Fine grained API token permissions', () => {
 | 
				
			|||||||
                const token = await stores.apiTokenStore.insert({
 | 
					                const token = await stores.apiTokenStore.insert({
 | 
				
			||||||
                    environment: '',
 | 
					                    environment: '',
 | 
				
			||||||
                    projects: [],
 | 
					                    projects: [],
 | 
				
			||||||
                    tokenName: '',
 | 
					                    tokenName: 'cilent',
 | 
				
			||||||
                    username: 'cilent',
 | 
					 | 
				
			||||||
                    secret: '*:environment.delete_client_token',
 | 
					                    secret: '*:environment.delete_client_token',
 | 
				
			||||||
                    type: ApiTokenType.CLIENT,
 | 
					                    type: ApiTokenType.CLIENT,
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
@ -1094,8 +916,7 @@ describe('Fine grained API token permissions', () => {
 | 
				
			|||||||
                const token = await stores.apiTokenStore.insert({
 | 
					                const token = await stores.apiTokenStore.insert({
 | 
				
			||||||
                    environment: '',
 | 
					                    environment: '',
 | 
				
			||||||
                    projects: [],
 | 
					                    projects: [],
 | 
				
			||||||
                    tokenName: '',
 | 
					                    tokenName: 'frontend',
 | 
				
			||||||
                    username: 'frontend',
 | 
					 | 
				
			||||||
                    secret: '*:environment.delete_frontend_token',
 | 
					                    secret: '*:environment.delete_frontend_token',
 | 
				
			||||||
                    type: ApiTokenType.FRONTEND,
 | 
					                    type: ApiTokenType.FRONTEND,
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
@ -1155,8 +976,7 @@ describe('Fine grained API token permissions', () => {
 | 
				
			|||||||
                const token = await stores.apiTokenStore.insert({
 | 
					                const token = await stores.apiTokenStore.insert({
 | 
				
			||||||
                    environment: '',
 | 
					                    environment: '',
 | 
				
			||||||
                    projects: [],
 | 
					                    projects: [],
 | 
				
			||||||
                    tokenName: '',
 | 
					                    tokenName: 'admin',
 | 
				
			||||||
                    username: 'admin',
 | 
					 | 
				
			||||||
                    secret: '*:*:delete_admin_token',
 | 
					                    secret: '*:*:delete_admin_token',
 | 
				
			||||||
                    type: ApiTokenType.ADMIN,
 | 
					                    type: ApiTokenType.ADMIN,
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
 | 
				
			|||||||
@ -52,14 +52,13 @@ test('creates new client token', async () => {
 | 
				
			|||||||
    return app.request
 | 
					    return app.request
 | 
				
			||||||
        .post('/api/admin/api-tokens')
 | 
					        .post('/api/admin/api-tokens')
 | 
				
			||||||
        .send({
 | 
					        .send({
 | 
				
			||||||
            username: 'default-client',
 | 
					            tokenName: 'default-client',
 | 
				
			||||||
            type: 'client',
 | 
					            type: 'client',
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .set('Content-Type', 'application/json')
 | 
					        .set('Content-Type', 'application/json')
 | 
				
			||||||
        .expect(201)
 | 
					        .expect(201)
 | 
				
			||||||
        .expect((res) => {
 | 
					        .expect((res) => {
 | 
				
			||||||
            expect(res.body.username).toBe('default-client');
 | 
					            expect(res.body.tokenName).toBe('default-client');
 | 
				
			||||||
            expect(res.body.tokenName).toBe(res.body.username);
 | 
					 | 
				
			||||||
            expect(res.body.type).toBe('client');
 | 
					            expect(res.body.type).toBe('client');
 | 
				
			||||||
            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);
 | 
				
			||||||
@ -70,7 +69,6 @@ test('update client token with expiry', async () => {
 | 
				
			|||||||
    const tokenSecret = '*:environment.random-secret-update';
 | 
					    const tokenSecret = '*:environment.random-secret-update';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await db.stores.apiTokenStore.insert({
 | 
					    await db.stores.apiTokenStore.insert({
 | 
				
			||||||
        username: 'test',
 | 
					 | 
				
			||||||
        projects: ['*'],
 | 
					        projects: ['*'],
 | 
				
			||||||
        tokenName: 'test_token',
 | 
					        tokenName: 'test_token',
 | 
				
			||||||
        secret: tokenSecret,
 | 
					        secret: tokenSecret,
 | 
				
			||||||
@ -104,7 +102,7 @@ test('creates a lot of client tokens', async () => {
 | 
				
			|||||||
            app.request
 | 
					            app.request
 | 
				
			||||||
                .post('/api/admin/api-tokens')
 | 
					                .post('/api/admin/api-tokens')
 | 
				
			||||||
                .send({
 | 
					                .send({
 | 
				
			||||||
                    username: 'default-client',
 | 
					                    tokenName: 'default-client',
 | 
				
			||||||
                    type: 'client',
 | 
					                    type: 'client',
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
                .set('Content-Type', 'application/json')
 | 
					                .set('Content-Type', 'application/json')
 | 
				
			||||||
@ -138,7 +136,6 @@ test('removes api token', async () => {
 | 
				
			|||||||
        environment: 'development',
 | 
					        environment: 'development',
 | 
				
			||||||
        projects: ['*'],
 | 
					        projects: ['*'],
 | 
				
			||||||
        tokenName: 'testtoken',
 | 
					        tokenName: 'testtoken',
 | 
				
			||||||
        username: 'test',
 | 
					 | 
				
			||||||
        secret: tokenSecret,
 | 
					        secret: tokenSecret,
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -161,7 +158,7 @@ test('creates new client token: project & environment defaults to "*"', async ()
 | 
				
			|||||||
    return app.request
 | 
					    return app.request
 | 
				
			||||||
        .post('/api/admin/api-tokens')
 | 
					        .post('/api/admin/api-tokens')
 | 
				
			||||||
        .send({
 | 
					        .send({
 | 
				
			||||||
            username: 'default-client',
 | 
					            tokenName: 'default-client',
 | 
				
			||||||
            type: 'client',
 | 
					            type: 'client',
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .set('Content-Type', 'application/json')
 | 
					        .set('Content-Type', 'application/json')
 | 
				
			||||||
@ -178,9 +175,9 @@ test('creates new client token with project & environment set', async () => {
 | 
				
			|||||||
    return app.request
 | 
					    return app.request
 | 
				
			||||||
        .post('/api/admin/api-tokens')
 | 
					        .post('/api/admin/api-tokens')
 | 
				
			||||||
        .send({
 | 
					        .send({
 | 
				
			||||||
            username: 'default-client',
 | 
					            tokenName: 'default-client',
 | 
				
			||||||
            type: 'client',
 | 
					            type: 'client',
 | 
				
			||||||
            project: 'default',
 | 
					            projects: ['default'],
 | 
				
			||||||
            environment: DEFAULT_ENV,
 | 
					            environment: DEFAULT_ENV,
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .set('Content-Type', 'application/json')
 | 
					        .set('Content-Type', 'application/json')
 | 
				
			||||||
@ -197,7 +194,7 @@ test('should prefix default token with "*:*."', async () => {
 | 
				
			|||||||
    return app.request
 | 
					    return app.request
 | 
				
			||||||
        .post('/api/admin/api-tokens')
 | 
					        .post('/api/admin/api-tokens')
 | 
				
			||||||
        .send({
 | 
					        .send({
 | 
				
			||||||
            username: 'default-client',
 | 
					            tokenName: 'default-client',
 | 
				
			||||||
            type: 'client',
 | 
					            type: 'client',
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .set('Content-Type', 'application/json')
 | 
					        .set('Content-Type', 'application/json')
 | 
				
			||||||
@ -211,9 +208,9 @@ test('should prefix token with "project:environment."', async () => {
 | 
				
			|||||||
    return app.request
 | 
					    return app.request
 | 
				
			||||||
        .post('/api/admin/api-tokens')
 | 
					        .post('/api/admin/api-tokens')
 | 
				
			||||||
        .send({
 | 
					        .send({
 | 
				
			||||||
            username: 'default-client',
 | 
					            tokenName: 'default-client',
 | 
				
			||||||
            type: 'client',
 | 
					            type: 'client',
 | 
				
			||||||
            project: 'default',
 | 
					            projects: ['default'],
 | 
				
			||||||
            environment: DEFAULT_ENV,
 | 
					            environment: DEFAULT_ENV,
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .set('Content-Type', 'application/json')
 | 
					        .set('Content-Type', 'application/json')
 | 
				
			||||||
@ -227,9 +224,9 @@ test('should not create token for invalid projectId', async () => {
 | 
				
			|||||||
    return app.request
 | 
					    return app.request
 | 
				
			||||||
        .post('/api/admin/api-tokens')
 | 
					        .post('/api/admin/api-tokens')
 | 
				
			||||||
        .send({
 | 
					        .send({
 | 
				
			||||||
            username: 'default-client',
 | 
					            tokenName: 'default-client',
 | 
				
			||||||
            type: 'client',
 | 
					            type: 'client',
 | 
				
			||||||
            project: 'bogus-project-something',
 | 
					            projects: ['bogus-project-something'],
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .set('Content-Type', 'application/json')
 | 
					        .set('Content-Type', 'application/json')
 | 
				
			||||||
        .expect(400)
 | 
					        .expect(400)
 | 
				
			||||||
@ -244,7 +241,7 @@ test('should not create token for invalid environment', async () => {
 | 
				
			|||||||
    return app.request
 | 
					    return app.request
 | 
				
			||||||
        .post('/api/admin/api-tokens')
 | 
					        .post('/api/admin/api-tokens')
 | 
				
			||||||
        .send({
 | 
					        .send({
 | 
				
			||||||
            username: 'default-client',
 | 
					            tokenName: 'default-client',
 | 
				
			||||||
            type: 'client',
 | 
					            type: 'client',
 | 
				
			||||||
            environment: 'bogus-environment-something',
 | 
					            environment: 'bogus-environment-something',
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
@ -257,22 +254,21 @@ test('should not create token for invalid environment', async () => {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('needs one of the username and tokenName properties set', async () => {
 | 
					test('needs tokenName properties set', async () => {
 | 
				
			||||||
    return app.request
 | 
					    return app.request
 | 
				
			||||||
        .post('/api/admin/api-tokens')
 | 
					        .post('/api/admin/api-tokens')
 | 
				
			||||||
        .send({
 | 
					        .send({
 | 
				
			||||||
            type: 'admin',
 | 
					            type: 'client',
 | 
				
			||||||
            environment: '*',
 | 
					            environment: '*',
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .set('Content-Type', 'application/json')
 | 
					        .set('Content-Type', 'application/json')
 | 
				
			||||||
        .expect(400);
 | 
					        .expect(400);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('only one of tokenName and username can be set', async () => {
 | 
					test('can not create token with admin type', async () => {
 | 
				
			||||||
    return app.request
 | 
					    return app.request
 | 
				
			||||||
        .post('/api/admin/api-tokens')
 | 
					        .post('/api/admin/api-tokens')
 | 
				
			||||||
        .send({
 | 
					        .send({
 | 
				
			||||||
            username: 'default-client-name',
 | 
					 | 
				
			||||||
            tokenName: 'default-token-name',
 | 
					            tokenName: 'default-token-name',
 | 
				
			||||||
            type: 'admin',
 | 
					            type: 'admin',
 | 
				
			||||||
            environment: '*',
 | 
					            environment: '*',
 | 
				
			||||||
@ -285,7 +281,7 @@ test('client tokens cannot span all environments', async () => {
 | 
				
			|||||||
    return app.request
 | 
					    return app.request
 | 
				
			||||||
        .post('/api/admin/api-tokens')
 | 
					        .post('/api/admin/api-tokens')
 | 
				
			||||||
        .send({
 | 
					        .send({
 | 
				
			||||||
            username: 'default-client',
 | 
					            tokenName: 'default-client',
 | 
				
			||||||
            type: 'client',
 | 
					            type: 'client',
 | 
				
			||||||
            environment: ALL,
 | 
					            environment: ALL,
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
@ -302,9 +298,9 @@ test('should create token for disabled environment', async () => {
 | 
				
			|||||||
    return app.request
 | 
					    return app.request
 | 
				
			||||||
        .post('/api/admin/api-tokens')
 | 
					        .post('/api/admin/api-tokens')
 | 
				
			||||||
        .send({
 | 
					        .send({
 | 
				
			||||||
            username: 'default',
 | 
					            tokenName: 'default',
 | 
				
			||||||
            type: 'client',
 | 
					            type: 'client',
 | 
				
			||||||
            project: 'default',
 | 
					            projects: ['default'],
 | 
				
			||||||
            environment: 'disabledEnvironment',
 | 
					            environment: 'disabledEnvironment',
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .set('Content-Type', 'application/json')
 | 
					        .set('Content-Type', 'application/json')
 | 
				
			||||||
 | 
				
			|||||||
@ -77,7 +77,7 @@ test('fails to create new client token when given wrong project', async () => {
 | 
				
			|||||||
    return app.request
 | 
					    return app.request
 | 
				
			||||||
        .post('/api/admin/projects/wrong/api-tokens')
 | 
					        .post('/api/admin/projects/wrong/api-tokens')
 | 
				
			||||||
        .send({
 | 
					        .send({
 | 
				
			||||||
            username: 'default-client',
 | 
					            tokenName: 'default-client',
 | 
				
			||||||
            type: 'client',
 | 
					            type: 'client',
 | 
				
			||||||
            projects: ['wrong'],
 | 
					            projects: ['wrong'],
 | 
				
			||||||
            environment: 'default',
 | 
					            environment: 'default',
 | 
				
			||||||
@ -90,7 +90,7 @@ test('creates new client token', async () => {
 | 
				
			|||||||
    return app.request
 | 
					    return app.request
 | 
				
			||||||
        .post('/api/admin/projects/default/api-tokens')
 | 
					        .post('/api/admin/projects/default/api-tokens')
 | 
				
			||||||
        .send({
 | 
					        .send({
 | 
				
			||||||
            username: 'default-client',
 | 
					            tokenName: 'default-client',
 | 
				
			||||||
            type: 'client',
 | 
					            type: 'client',
 | 
				
			||||||
            projects: ['default'],
 | 
					            projects: ['default'],
 | 
				
			||||||
            environment: 'default',
 | 
					            environment: 'default',
 | 
				
			||||||
@ -98,7 +98,7 @@ test('creates new client token', async () => {
 | 
				
			|||||||
        .set('Content-Type', 'application/json')
 | 
					        .set('Content-Type', 'application/json')
 | 
				
			||||||
        .expect(201)
 | 
					        .expect(201)
 | 
				
			||||||
        .expect((res) => {
 | 
					        .expect((res) => {
 | 
				
			||||||
            expect(res.body.username).toBe('default-client');
 | 
					            expect(res.body.tokenName).toBe('default-client');
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -125,11 +125,11 @@ afterAll(async () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('returns feature flag with "default" config', async () => {
 | 
					test('returns feature flag with "default" config', async () => {
 | 
				
			||||||
    const token = await apiTokenService.createApiToken({
 | 
					    const token = await apiTokenService.createApiTokenWithProjects({
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
        tokenName,
 | 
					        tokenName,
 | 
				
			||||||
        environment: DEFAULT_ENV,
 | 
					        environment: DEFAULT_ENV,
 | 
				
			||||||
        project,
 | 
					        projects: [project],
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    await app.request
 | 
					    await app.request
 | 
				
			||||||
        .get('/api/client/features')
 | 
					        .get('/api/client/features')
 | 
				
			||||||
@ -147,11 +147,11 @@ test('returns feature flag with "default" config', async () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('returns feature flag with testing environment config', async () => {
 | 
					test('returns feature flag with testing environment config', async () => {
 | 
				
			||||||
    const token = await apiTokenService.createApiToken({
 | 
					    const token = await apiTokenService.createApiTokenWithProjects({
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
        tokenName: tokenName,
 | 
					        tokenName: tokenName,
 | 
				
			||||||
        environment,
 | 
					        environment,
 | 
				
			||||||
        project,
 | 
					        projects: [project],
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    await app.request
 | 
					    await app.request
 | 
				
			||||||
        .get('/api/client/features')
 | 
					        .get('/api/client/features')
 | 
				
			||||||
@ -173,11 +173,11 @@ 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.createApiToken({
 | 
					    const token = await apiTokenService.createApiTokenWithProjects({
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
        tokenName: tokenName,
 | 
					        tokenName: tokenName,
 | 
				
			||||||
        environment,
 | 
					        environment,
 | 
				
			||||||
        project: project2,
 | 
					        projects: [project2],
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    await app.request
 | 
					    await app.request
 | 
				
			||||||
        .get('/api/client/features')
 | 
					        .get('/api/client/features')
 | 
				
			||||||
@ -193,11 +193,11 @@ 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.createApiToken({
 | 
					    const token = await apiTokenService.createApiTokenWithProjects({
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
        tokenName: tokenName,
 | 
					        tokenName: tokenName,
 | 
				
			||||||
        environment,
 | 
					        environment,
 | 
				
			||||||
        project: '*',
 | 
					        projects: ['*'],
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    await app.request
 | 
					    await app.request
 | 
				
			||||||
        .get('/api/client/features')
 | 
					        .get('/api/client/features')
 | 
				
			||||||
 | 
				
			|||||||
@ -133,11 +133,11 @@ afterAll(async () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('doesnt return feature flags if project deleted', async () => {
 | 
					test('doesnt return feature flags if project deleted', async () => {
 | 
				
			||||||
    const token = await apiTokenService.createApiToken({
 | 
					    const token = await apiTokenService.createApiTokenWithProjects({
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
        tokenName: deletionTokenName,
 | 
					        tokenName: deletionTokenName,
 | 
				
			||||||
        environment,
 | 
					        environment,
 | 
				
			||||||
        project: deletionProject,
 | 
					        projects: [deletionProject],
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await app.services.projectService.deleteProject(
 | 
					    await app.services.projectService.deleteProject(
 | 
				
			||||||
 | 
				
			|||||||
@ -31,11 +31,11 @@ test('should enrich metrics with environment from api-token', async () => {
 | 
				
			|||||||
        type: 'test',
 | 
					        type: 'test',
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const token = await apiTokenService.createApiToken({
 | 
					    const token = await apiTokenService.createApiTokenWithProjects({
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
        tokenName: 'test',
 | 
					        tokenName: 'test',
 | 
				
			||||||
        environment: 'some',
 | 
					        environment: 'some',
 | 
				
			||||||
        project: '*',
 | 
					        projects: ['*'],
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const featureName = Object.keys(metricsExample.bucket.toggles)[0];
 | 
					    const featureName = Object.keys(metricsExample.bucket.toggles)[0];
 | 
				
			||||||
 | 
				
			|||||||
@ -74,12 +74,14 @@ test('should accept client metrics', async () => {
 | 
				
			|||||||
test('should pick up environment from token', async () => {
 | 
					test('should pick up environment from token', async () => {
 | 
				
			||||||
    const environment = 'test';
 | 
					    const environment = 'test';
 | 
				
			||||||
    await db.stores.environmentStore.create({ name: 'test', type: 'test' });
 | 
					    await db.stores.environmentStore.create({ name: 'test', type: 'test' });
 | 
				
			||||||
    const token = await app.services.apiTokenService.createApiToken({
 | 
					    const token = await app.services.apiTokenService.createApiTokenWithProjects(
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        {
 | 
				
			||||||
        project: 'default',
 | 
					            type: ApiTokenType.CLIENT,
 | 
				
			||||||
        environment,
 | 
					            projects: ['default'],
 | 
				
			||||||
        tokenName: 'tester',
 | 
					            environment,
 | 
				
			||||||
    });
 | 
					            tokenName: 'tester',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // @ts-expect-error - cachedFeatureNames is a private property in ClientMetricsServiceV2
 | 
					    // @ts-expect-error - cachedFeatureNames is a private property in ClientMetricsServiceV2
 | 
				
			||||||
    app.services.clientMetricsServiceV2.cachedFeatureNames = vi
 | 
					    app.services.clientMetricsServiceV2.cachedFeatureNames = vi
 | 
				
			||||||
@ -129,12 +131,14 @@ test('should set lastSeen for toggles with metrics both for toggle and toggle en
 | 
				
			|||||||
        .fn<() => Promise<string[]>>()
 | 
					        .fn<() => Promise<string[]>>()
 | 
				
			||||||
        .mockResolvedValue(['t1', 't2']);
 | 
					        .mockResolvedValue(['t1', 't2']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const token = await app.services.apiTokenService.createApiToken({
 | 
					    const token = await app.services.apiTokenService.createApiTokenWithProjects(
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        {
 | 
				
			||||||
        project: 'default',
 | 
					            type: ApiTokenType.CLIENT,
 | 
				
			||||||
        environment: 'default',
 | 
					            projects: ['default'],
 | 
				
			||||||
        tokenName: 'tester',
 | 
					            environment: 'default',
 | 
				
			||||||
    });
 | 
					            tokenName: 'tester',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await app.request
 | 
					    await app.request
 | 
				
			||||||
        .post('/api/client/metrics')
 | 
					        .post('/api/client/metrics')
 | 
				
			||||||
 | 
				
			|||||||
@ -68,10 +68,10 @@ test('should have empty list of tokens', async () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('should create client token', async () => {
 | 
					test('should create client token', async () => {
 | 
				
			||||||
    const token = await apiTokenService.createApiToken({
 | 
					    const token = await apiTokenService.createApiTokenWithProjects({
 | 
				
			||||||
        tokenName: 'default-client',
 | 
					        tokenName: 'default-client',
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
        project: '*',
 | 
					        projects: ['*'],
 | 
				
			||||||
        environment: DEFAULT_ENV,
 | 
					        environment: DEFAULT_ENV,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    const allTokens = await apiTokenService.getAllTokens();
 | 
					    const allTokens = await apiTokenService.getAllTokens();
 | 
				
			||||||
@ -79,15 +79,15 @@ 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.CLIENT);
 | 
				
			||||||
    expect(token.username).toBe('default-client');
 | 
					    expect(token.tokenName).toBe('default-client');
 | 
				
			||||||
    expect(allTokens[0].secret).toBe(token.secret);
 | 
					    expect(allTokens[0].secret).toBe(token.secret);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('should create admin token', async () => {
 | 
					test('should create admin token', async () => {
 | 
				
			||||||
    const token = await apiTokenService.createApiToken({
 | 
					    const token = await apiTokenService.createApiTokenWithProjects({
 | 
				
			||||||
        tokenName: 'admin',
 | 
					        tokenName: 'admin',
 | 
				
			||||||
        type: ApiTokenType.ADMIN,
 | 
					        type: ApiTokenType.ADMIN,
 | 
				
			||||||
        project: '*',
 | 
					        projects: ['*'],
 | 
				
			||||||
        environment: '*',
 | 
					        environment: '*',
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -97,11 +97,11 @@ test('should create admin token', async () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
test('should set expiry of token', async () => {
 | 
					test('should set expiry of token', async () => {
 | 
				
			||||||
    const time = new Date('2022-01-01');
 | 
					    const time = new Date('2022-01-01');
 | 
				
			||||||
    await apiTokenService.createApiToken({
 | 
					    await apiTokenService.createApiTokenWithProjects({
 | 
				
			||||||
        tokenName: 'default-client',
 | 
					        tokenName: 'default-client',
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
        expiresAt: time,
 | 
					        expiresAt: time,
 | 
				
			||||||
        project: '*',
 | 
					        projects: ['*'],
 | 
				
			||||||
        environment: DEFAULT_ENV,
 | 
					        environment: DEFAULT_ENV,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -114,12 +114,12 @@ test('should update expiry of token', async () => {
 | 
				
			|||||||
    const time = new Date('2022-01-01');
 | 
					    const time = new Date('2022-01-01');
 | 
				
			||||||
    const newTime = new Date('2023-01-01');
 | 
					    const newTime = new Date('2023-01-01');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const token = await apiTokenService.createApiToken(
 | 
					    const token = await apiTokenService.createApiTokenWithProjects(
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            tokenName: 'default-client',
 | 
					            tokenName: 'default-client',
 | 
				
			||||||
            type: ApiTokenType.CLIENT,
 | 
					            type: ApiTokenType.CLIENT,
 | 
				
			||||||
            expiresAt: time,
 | 
					            expiresAt: time,
 | 
				
			||||||
            project: '*',
 | 
					            projects: ['*'],
 | 
				
			||||||
            environment: DEFAULT_ENV,
 | 
					            environment: DEFAULT_ENV,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        TEST_AUDIT_USER,
 | 
					        TEST_AUDIT_USER,
 | 
				
			||||||
@ -133,7 +133,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.createApiToken({
 | 
					    const token = await apiTokenService.createApiTokenWithProjects({
 | 
				
			||||||
        tokenName: 'default-client',
 | 
					        tokenName: 'default-client',
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
        projects: ['default', 'test-project'],
 | 
					        projects: ['default', 'test-project'],
 | 
				
			||||||
@ -145,7 +145,7 @@ test('should create client token with project list', async () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('should strip all other projects if ALL_PROJECTS is present', async () => {
 | 
					test('should strip all other projects if ALL_PROJECTS is present', async () => {
 | 
				
			||||||
    const token = await apiTokenService.createApiToken({
 | 
					    const token = await apiTokenService.createApiTokenWithProjects({
 | 
				
			||||||
        tokenName: 'default-client',
 | 
					        tokenName: 'default-client',
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
        projects: ['*', 'default'],
 | 
					        projects: ['*', 'default'],
 | 
				
			||||||
@ -159,21 +159,23 @@ test('should return user with multiple projects', async () => {
 | 
				
			|||||||
    const now = Date.now();
 | 
					    const now = Date.now();
 | 
				
			||||||
    const tomorrow = addDays(now, 1);
 | 
					    const tomorrow = addDays(now, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { secret: secret1 } = await apiTokenService.createApiToken({
 | 
					    const { secret: secret1 } =
 | 
				
			||||||
        tokenName: 'default-valid',
 | 
					        await apiTokenService.createApiTokenWithProjects({
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					            tokenName: 'default-valid',
 | 
				
			||||||
        expiresAt: tomorrow,
 | 
					            type: ApiTokenType.CLIENT,
 | 
				
			||||||
        projects: ['test-project', 'default'],
 | 
					            expiresAt: tomorrow,
 | 
				
			||||||
        environment: DEFAULT_ENV,
 | 
					            projects: ['test-project', 'default'],
 | 
				
			||||||
    });
 | 
					            environment: DEFAULT_ENV,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { secret: secret2 } = await apiTokenService.createApiToken({
 | 
					    const { secret: secret2 } =
 | 
				
			||||||
        tokenName: 'default-also-valid',
 | 
					        await apiTokenService.createApiTokenWithProjects({
 | 
				
			||||||
        type: ApiTokenType.CLIENT,
 | 
					            tokenName: 'default-also-valid',
 | 
				
			||||||
        expiresAt: tomorrow,
 | 
					            type: ApiTokenType.CLIENT,
 | 
				
			||||||
        projects: ['test-project'],
 | 
					            expiresAt: tomorrow,
 | 
				
			||||||
        environment: DEFAULT_ENV,
 | 
					            projects: ['test-project'],
 | 
				
			||||||
    });
 | 
					            environment: DEFAULT_ENV,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const multiProjectUser = await apiTokenService.getUserForToken(secret1);
 | 
					    const multiProjectUser = await apiTokenService.getUserForToken(secret1);
 | 
				
			||||||
    const singleProjectUser = await apiTokenService.getUserForToken(secret2);
 | 
					    const singleProjectUser = await apiTokenService.getUserForToken(secret2);
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user