1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-07 01:16:28 +02:00

chore(apitoken)!: remove ILegacyApiTokenCreate (#10072)

This commit is contained in:
David Leek 2025-06-04 11:41:37 +02:00 committed by GitHub
parent 37548c3436
commit 1aadbb3641
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 256 additions and 587 deletions

View File

@ -27,7 +27,7 @@ test('should create default config', async () => {
test('should add initApiToken for admin token from options', async () => {
const token = {
environment: '*',
project: '*',
projects: ['*'],
secret: '*:*.some-random-string',
type: ApiTokenType.ADMIN,
tokenName: 'admin',
@ -52,7 +52,9 @@ test('should add initApiToken for admin token from options', async () => {
expect(config.authentication.initApiTokens[0].environment).toBe(
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(
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 () => {
const token = {
environment: 'development',
project: 'default',
projects: ['default'],
secret: 'default:development.some-random-string',
type: ApiTokenType.CLIENT,
tokenName: 'admin',
@ -86,7 +88,9 @@ test('should add initApiToken for client token from options', async () => {
expect(config.authentication.initApiTokens[0].environment).toBe(
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(
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[0].environment).toBe('*');
expect(config.authentication.initApiTokens[0].project).toBe('*');
expect(config.authentication.initApiTokens[0].projects).toMatchObject([
'*',
]);
expect(config.authentication.initApiTokens[0].type).toBe(
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';
const token = {
environment: '*',
project: '*',
projects: ['*'],
secret: '*:*.some-random-string',
type: ApiTokenType.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(
'development',
);
expect(config.authentication.initApiTokens[0].project).toBe('default');
expect(config.authentication.initApiTokens[0].projects).toMatchObject([
'default',
]);
expect(config.authentication.initApiTokens[0].type).toBe(
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 () => {
const token = {
environment: '*',
project: '*',
projects: ['*'],
secret: '*:*.some-random-string',
type: ApiTokenType.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';
const token = {
environment: '*',
project: '*',
projects: ['*'],
secret: '*:*.some-random-string',
type: ApiTokenType.ADMIN,
tokenName: 'admin',

View File

@ -37,7 +37,7 @@ import {
secondsToMilliseconds,
} from 'date-fns';
import EventEmitter from 'events';
import { mapLegacyToken, validateApiToken } from './types/models/api-token.js';
import { validateApiToken } from './types/models/api-token.js';
import {
parseEnvVarBoolean,
parseEnvVarJSON,
@ -417,13 +417,13 @@ const loadTokensFromString = (
const [environment = '*'] = rest.split('.');
const token = {
createdAt: undefined,
project,
projects: [project],
environment,
secret,
type: tokenType,
tokenName: 'admin',
};
validateApiToken(mapLegacyToken(token));
validateApiToken(token);
return token;
});
return tokens;

View File

@ -50,7 +50,6 @@ const tokenRowReducer = (acc, tokenRow) => {
createdAt: token.created_at,
alias: token.alias,
seenAt: token.seen_at,
username: token.token_name ? token.token_name : token.username,
};
}
const currentToken = acc[tokenRow.secret];
@ -65,8 +64,8 @@ const tokenRowReducer = (acc, tokenRow) => {
};
const toRow = (newToken: IApiTokenCreate) => ({
username: newToken.tokenName ?? newToken.username,
token_name: newToken.tokenName ?? newToken.username,
username: newToken.tokenName,
token_name: newToken.tokenName,
secret: newToken.secret,
type: newToken.type,
environment:
@ -192,7 +191,6 @@ export class ApiTokenStore implements IApiTokenStore {
await Promise.all(updateProjectTasks);
return {
...newToken,
username: newToken.tokenName,
alias: newToken.alias || null,
project: newToken.projects?.join(',') || '*',
createdAt: row.created_at,

View File

@ -2436,11 +2436,11 @@ test('should also delete api tokens that were only bound to deleted project', as
auditUser,
);
const token = await apiTokenService.createApiToken({
const token = await apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT,
tokenName,
environment: DEFAULT_ENV,
project: project,
projects: [project],
});
await projectService.deleteProject(project, user, auditUser);
@ -2471,7 +2471,7 @@ test('should not delete project-bound api tokens still bound to project', async
auditUser,
);
const token = await apiTokenService.createApiToken({
const token = await apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT,
tokenName,
environment: DEFAULT_ENV,
@ -2507,7 +2507,7 @@ test('should delete project-bound api tokens when all projects they belong to ar
auditUser,
);
const token = await apiTokenService.createApiToken({
const token = await apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT,
tokenName,
environment: DEFAULT_ENV,

View File

@ -4,7 +4,6 @@ import type { ApiTokenSchema } from './api-token-schema.js';
const defaultData: ApiTokenSchema = {
secret: '',
username: '',
tokenName: '',
type: ApiTokenType.CLIENT,
environment: '',

View File

@ -5,14 +5,7 @@ export const apiTokenSchema = {
$id: '#/components/schemas/apiTokenSchema',
type: 'object',
additionalProperties: false,
required: [
'secret',
'tokenName',
'type',
'project',
'projects',
'createdAt',
],
required: ['secret', 'tokenName', 'type', 'projects', 'createdAt'],
description:
'An overview of an [Unleash API token](https://docs.getunleash.io/reference/api-tokens-and-client-keys).',
properties: {
@ -21,13 +14,6 @@ export const apiTokenSchema = {
description: 'The token used for authentication.',
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: {
type: 'string',
description: 'A unique name for this particular token',

View File

@ -1,17 +1,5 @@
import type { FromSchema } from 'json-schema-to-ts';
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 = {
type: 'object',
@ -25,20 +13,6 @@ const tokenNameSchema = {
},
} 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 = {
required: ['type'],
type: 'object',
@ -100,12 +74,7 @@ export const createApiTokenSchema = {
type: 'object',
description:
'The data required to create an [Unleash API token](https://docs.getunleash.io/reference/api-tokens-and-client-keys).',
oneOf: [
mergeAllOfs([expireSchema, adminSchema, tokenNameSchema]),
mergeAllOfs([expireSchema, adminSchema, usernameSchema]),
mergeAllOfs([expireSchema, clientFrontendSchema, tokenNameSchema]),
mergeAllOfs([expireSchema, clientFrontendSchema, usernameSchema]),
],
oneOf: [mergeAllOfs([expireSchema, clientFrontendSchema, tokenNameSchema])],
components: {},
} as const;

View 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
>;

View File

@ -53,6 +53,7 @@ export * from './create-feature-strategy-schema.js';
export * from './create-group-schema.js';
export * from './create-invited-user-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-variant-schema.js';
export * from './create-tag-schema.js';

View File

@ -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();
});
});
});

View File

@ -43,6 +43,7 @@ import {
} from '../../openapi/util/standard-responses.js';
import type { FrontendApiService } from '../../features/frontend-api/frontend-api-service.js';
import { OperationDeniedError } from '../../error/index.js';
import type { CreateApiTokenSchema } from '../../internals.js';
interface TokenParam {
token: string;
@ -299,24 +300,20 @@ export class ApiTokenController extends Controller {
}
async createApiToken(
req: IAuthRequest,
req: IAuthRequest<CreateApiTokenSchema>,
res: Response<ApiTokenSchema>,
): Promise<any> {
const createToken = await createApiToken.validateAsync(req.body);
const permissionRequired = tokenTypeToCreatePermission(
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(
req.user,
permissionRequired,
);
if (hasPermission) {
const token = await this.apiTokenService.createApiToken(
const token = await this.apiTokenService.createApiTokenWithProjects(
createToken,
req.audit,
);

View File

@ -32,8 +32,9 @@ import Controller from '../../controller.js';
import type { Logger } from '../../../logger.js';
import type { Response } from 'express';
import { timingSafeEqual } from 'crypto';
import { createApiToken } from '../../../schema/api-token-schema.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 {
token: string;
@ -109,7 +110,9 @@ export class ProjectApiTokenController extends Controller {
openApiService.validPath({
tags: ['Projects'],
operationId: 'createProjectApiToken',
requestBody: createRequestSchema('createApiTokenSchema'),
requestBody: createRequestSchema(
'createProjectApiTokenSchema',
),
summary: 'Create a project API token.',
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.',
@ -160,10 +163,10 @@ export class ProjectApiTokenController extends Controller {
}
async createProjectApiToken(
req: IAuthRequest,
req: IAuthRequest<{ projectId: string }, CreateProjectApiTokenSchema>,
res: Response<ApiTokenSchema>,
): Promise<any> {
const createToken = await createApiToken.validateAsync(req.body);
const createToken = await createProjectApiToken.validateAsync(req.body);
const { projectId } = req.params;
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]`,
);
}
if (!createToken.project) {
createToken.project = projectId;
}
if (
createToken.projects.length === 1 &&
createToken.projects[0] === projectId
) {
const token = await this.apiTokenService.createApiToken(
createToken,
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);
}
const token = await this.apiTokenService.createApiTokenWithProjects(
{ ...createToken, projects: [projectId] },
req.audit,
);
this.openApiService.respondWithValidation(
201,
res,
apiTokenSchema.$id,
serializeDates(token),
{ location: `api-tokens` },
);
}
async deleteProjectApiToken(

View File

@ -1,51 +1,50 @@
import { ALL } from '../types/models/api-token.js';
import { createApiToken } from './api-token-schema.js';
test('should reject token with projects and project', async () => {
expect.assertions(1);
test('should ignore token extra project field', async () => {
expect.assertions(0);
try {
await createApiToken.validateAsync({
username: 'test',
type: 'admin',
tokenName: 'test',
type: 'client',
project: 'default',
projects: ['default'],
});
} catch (error) {
expect(error.details[0].message).toEqual(
'"project" must not exist simultaneously with [projects]',
);
expect(error).toBeUndefined();
}
});
test('should not have default project set if projects is present', async () => {
const token = await createApiToken.validateAsync({
username: 'test',
type: 'admin',
tokenName: 'test',
type: 'client',
projects: ['default'],
});
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({
username: 'test',
type: 'admin',
tokenName: 'test',
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({
username: 'test',
type: 'admin',
tokenName: 'test',
type: 'client',
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 () => {
const token = await createApiToken.validateAsync({
username: 'test',
tokenName: 'test',
type: 'frontend',
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 () => {
const token = await createApiToken.validateAsync({
username: 'test',
tokenName: 'test',
type: 'frontend',
project: 'default',
});
expect(token.environment).toEqual('default');
});

View File

@ -6,29 +6,18 @@ import { DEFAULT_ENV } from '../util/constants.js';
export const createApiToken = joi
.object()
.keys({
username: joi.string().optional(),
tokenName: joi.string().optional(),
tokenName: joi.string().required(),
type: joi
.string()
.lowercase()
.required()
.valid(
ApiTokenType.ADMIN,
ApiTokenType.CLIENT,
ApiTokenType.FRONTEND,
),
.valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND),
expiresAt: joi.date().optional(),
project: joi.when('projects', {
not: joi.required(),
then: joi.string().optional().default(ALL),
}),
projects: joi.array().min(0).optional(),
projects: joi.array().min(1).optional().default([ALL]),
environment: joi.when('type', {
is: joi.string().valid(ApiTokenType.CLIENT, ApiTokenType.FRONTEND),
then: joi.string().optional().default(DEFAULT_ENV),
otherwise: joi.string().optional().default(ALL),
}),
})
.nand('username', 'tokenName')
.nand('project', 'projects')
.options({ stripUnknown: true, allowUnknown: false, abortEarly: false });

View 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 });

View File

@ -19,7 +19,7 @@ import { vi } from 'vitest';
test('Should init api token', async () => {
const token = {
environment: '*',
project: '*',
projects: ['*'],
secret: '*:*:some-random-string',
type: ApiTokenType.ADMIN,
tokenName: 'admin',

View File

@ -5,11 +5,9 @@ import type { IUnleashStores } from '../types/stores.js';
import type { IUnleashConfig } from '../types/option.js';
import ApiUser, { type IApiUser } from '../types/api-user.js';
import {
type ILegacyApiTokenCreate,
resolveValidProjects,
validateApiToken,
validateApiTokenEnvironment,
mapLegacyToken,
mapLegacyTokenWithSecret,
} from '../types/models/api-token.js';
import type { IApiTokenStore } from '../types/stores/api-token-store.js';
import { FOREIGN_KEY_VIOLATION } from '../error/db-error.js';
@ -194,7 +192,7 @@ export class ApiTokenService {
return this.store.getAll();
}
async initApiTokens(tokens: ILegacyApiTokenCreate[]) {
async initApiTokens(tokens: IApiTokenCreate[]) {
const tokenCount = await this.store.count();
if (tokenCount > 0) {
this.logger.debug(
@ -203,9 +201,9 @@ export class ApiTokenService {
return;
}
try {
const createAll = tokens
.map(mapLegacyTokenWithSecret)
.map((t) => this.insertNewApiToken(t, SYSTEM_USER_AUDIT));
const createAll = tokens.map((t) =>
this.insertNewApiToken(t, SYSTEM_USER_AUDIT),
);
await Promise.all(createAll);
this.logger.info(
`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 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'>,
auditUser: IAuditUser = SYSTEM_USER_AUDIT,
): Promise<IApiToken> {
return this.internalCreateApiTokenWithProjects(newToken, auditUser);
return this.internalCreateApiTokenWithProjects(
{
...newToken,
projects: resolveValidProjects(newToken.projects),
},
auditUser,
);
}
private async internalCreateApiTokenWithProjects(

View File

@ -210,10 +210,6 @@ export interface IApiTokenCreate {
environment: string;
projects: string[];
expiresAt?: Date;
/**
* @deprecated Use tokenName instead
*/
username?: string;
}
export interface IApiToken extends Omit<IApiTokenCreate, 'alias'> {

View File

@ -4,64 +4,16 @@ import { ApiTokenType } from '../model.js';
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 => {
return projects && projects.length === 1 && projects[0] === ALL;
};
export const mapLegacyProjects = (
project?: string,
projects?: string[],
): 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',
);
export const resolveValidProjects = (projects: string[]): string[] => {
if (projects.includes('*')) {
return ['*'];
}
return cleanedProjects;
};
export const mapLegacyToken = (
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,
};
return projects;
};
export const validateApiToken = ({

View File

@ -1,7 +1,7 @@
import type { Express } from 'express';
import type EventEmitter from 'events';
import type { LogLevel, LogProvider } from '../logger.js';
import type { ILegacyApiTokenCreate } from './models/api-token.js';
import type { IApiTokenCreate } from './model.js';
import type {
IExperimentalOptions,
IFlagContext,
@ -85,7 +85,7 @@ export interface IAuthOption {
customAuthHandler?: CustomAuthHandler;
createAdminUser?: boolean;
initialAdminUser?: UsernameAdminUser;
initApiTokens: ILegacyApiTokenCreate[];
initApiTokens: IApiTokenCreate[];
}
export interface IImportOption {

View File

@ -63,7 +63,7 @@ process.nextTick(async () => {
initApiTokens: [
{
environment: '*',
project: '*',
projects: ['*'],
secret: '*:*.964a287e1b728cb5f4f3e0120df92cb5',
type: ApiTokenType.ADMIN,
tokenName: 'some-user',

View File

@ -1,7 +1,4 @@
import {
setupAppWithAuth,
setupAppWithCustomAuth,
} from '../../helpers/test-helper.js';
import { setupAppWithCustomAuth } from '../../helpers/test-helper.js';
import dbInit, { type ITestDb } from '../../helpers/database-init.js';
import getLogger from '../../../fixtures/no-logger.js';
import { ApiTokenType } from '../../../../lib/types/model.js';
@ -16,7 +13,6 @@ import {
SYSTEM_USER,
SYSTEM_USER_AUDIT,
SYSTEM_USER_ID,
TEST_AUDIT_USER,
UPDATE_CLIENT_API_TOKEN,
} from '../../../../lib/types/index.js';
import { addDays } from 'date-fns';
@ -79,8 +75,7 @@ test('editor users should only get client or frontend tokens', async () => {
await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'test',
tokenName: 'test',
secret: '*:environment.1234',
type: ApiTokenType.CLIENT,
});
@ -88,8 +83,7 @@ test('editor users should only get client or frontend tokens', async () => {
await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'frontend',
tokenName: 'frontend',
secret: '*:environment.12345',
type: ApiTokenType.FRONTEND,
});
@ -97,8 +91,7 @@ test('editor users should only get client or frontend tokens', async () => {
await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'test',
tokenName: 'test',
secret: '*:*.sdfsdf2d',
type: ApiTokenType.ADMIN,
});
@ -141,8 +134,7 @@ test('viewer users should not be allowed to fetch tokens', async () => {
await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'test',
tokenName: 'test',
secret: '*:environment.1234',
type: ApiTokenType.CLIENT,
});
@ -150,8 +142,7 @@ test('viewer users should not be allowed to fetch tokens', async () => {
await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'test',
tokenName: 'test',
secret: '*:*.sdfsdf2d',
type: ApiTokenType.ADMIN,
});
@ -164,102 +155,6 @@ test('viewer users should not be allowed to fetch tokens', async () => {
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 () => {
expect.assertions(0);
@ -312,7 +207,7 @@ test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', asyn
await request
.post('/api/admin/projects/default/api-tokens')
.send({
username: 'client-token-maker',
tokenName: 'client-token-maker',
type: 'client',
projects: ['default'],
})
@ -374,7 +269,7 @@ describe('Fine grained API token permissions', () => {
await request
.post('/api/admin/api-tokens')
.send({
username: 'default-client',
tokenName: 'default-client',
type: 'client',
})
.set('Content-Type', 'application/json')
@ -431,70 +326,13 @@ describe('Fine grained API token permissions', () => {
await request
.post('/api/admin/api-tokens')
.send({
username: 'default-frontend',
tokenName: 'default-frontend',
type: 'frontend',
})
.set('Content-Type', 'application/json')
.expect(403);
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', () => {
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({
environment: '',
projects: [],
tokenName: '',
username: 'client',
tokenName: 'client',
secret: '*:environment.client_secret',
type: ApiTokenType.CLIENT,
});
@ -556,16 +393,14 @@ describe('Fine grained API token permissions', () => {
await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'admin',
tokenName: 'admin',
secret: '*:*.sdfsdf2admin_secret',
type: ApiTokenType.ADMIN,
});
await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'frontender',
tokenName: 'frontender',
secret: '*:environment:sdfsdf2dfrontend_Secret',
type: ApiTokenType.FRONTEND,
});
@ -631,8 +466,7 @@ describe('Fine grained API token permissions', () => {
await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'client',
tokenName: 'client',
secret: '*:environment.client_secret_1234',
type: ApiTokenType.CLIENT,
});
@ -640,16 +474,14 @@ describe('Fine grained API token permissions', () => {
await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'admin',
tokenName: 'admin',
secret: '*:*.admin_secret_1234',
type: ApiTokenType.ADMIN,
});
await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'frontender',
tokenName: 'frontender',
secret: '*:environment.frontend_secret_1234',
type: ApiTokenType.FRONTEND,
});
@ -693,8 +525,7 @@ describe('Fine grained API token permissions', () => {
await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'client',
tokenName: 'client',
secret: '*:environment.client_secret_4321',
type: ApiTokenType.CLIENT,
});
@ -702,16 +533,14 @@ describe('Fine grained API token permissions', () => {
await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'admin',
tokenName: 'admin',
secret: '*:*.admin_secret_4321',
type: ApiTokenType.ADMIN,
});
await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'frontender',
tokenName: 'frontender',
secret: '*:environment.frontend_secret_4321',
type: ApiTokenType.FRONTEND,
});
@ -754,24 +583,21 @@ describe('Fine grained API token permissions', () => {
await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'client',
tokenName: 'client',
secret: '*:environment.client_secret_4321',
type: ApiTokenType.CLIENT,
});
await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'admin',
tokenName: 'admin',
secret: '*:*.admin_secret_4321',
type: ApiTokenType.ADMIN,
});
await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'frontender',
tokenName: 'frontender',
secret: '*:environment.frontend_secret_4321',
type: ApiTokenType.FRONTEND,
});
@ -842,8 +668,7 @@ describe('Fine grained API token permissions', () => {
const token = await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'cilent',
tokenName: 'cilent',
secret: '*:environment.update_client_token',
type: ApiTokenType.CLIENT,
});
@ -904,8 +729,7 @@ describe('Fine grained API token permissions', () => {
const token = await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'frontend',
tokenName: 'frontend',
secret: '*:environment.update_frontend_token',
type: ApiTokenType.FRONTEND,
});
@ -966,9 +790,8 @@ describe('Fine grained API token permissions', () => {
const token = await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'admin',
tokenName: 'admin',
secret: '*:*.update_admin_token',
type: ApiTokenType.ADMIN,
});
@ -1032,8 +855,7 @@ describe('Fine grained API token permissions', () => {
const token = await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'cilent',
tokenName: 'cilent',
secret: '*:environment.delete_client_token',
type: ApiTokenType.CLIENT,
});
@ -1094,8 +916,7 @@ describe('Fine grained API token permissions', () => {
const token = await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'frontend',
tokenName: 'frontend',
secret: '*:environment.delete_frontend_token',
type: ApiTokenType.FRONTEND,
});
@ -1155,8 +976,7 @@ describe('Fine grained API token permissions', () => {
const token = await stores.apiTokenStore.insert({
environment: '',
projects: [],
tokenName: '',
username: 'admin',
tokenName: 'admin',
secret: '*:*:delete_admin_token',
type: ApiTokenType.ADMIN,
});

View File

@ -52,14 +52,13 @@ test('creates new client token', async () => {
return app.request
.post('/api/admin/api-tokens')
.send({
username: 'default-client',
tokenName: 'default-client',
type: 'client',
})
.set('Content-Type', 'application/json')
.expect(201)
.expect((res) => {
expect(res.body.username).toBe('default-client');
expect(res.body.tokenName).toBe(res.body.username);
expect(res.body.tokenName).toBe('default-client');
expect(res.body.type).toBe('client');
expect(res.body.createdAt).toBeTruthy();
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';
await db.stores.apiTokenStore.insert({
username: 'test',
projects: ['*'],
tokenName: 'test_token',
secret: tokenSecret,
@ -104,7 +102,7 @@ test('creates a lot of client tokens', async () => {
app.request
.post('/api/admin/api-tokens')
.send({
username: 'default-client',
tokenName: 'default-client',
type: 'client',
})
.set('Content-Type', 'application/json')
@ -138,7 +136,6 @@ test('removes api token', async () => {
environment: 'development',
projects: ['*'],
tokenName: 'testtoken',
username: 'test',
secret: tokenSecret,
type: ApiTokenType.CLIENT,
});
@ -161,7 +158,7 @@ test('creates new client token: project & environment defaults to "*"', async ()
return app.request
.post('/api/admin/api-tokens')
.send({
username: 'default-client',
tokenName: 'default-client',
type: 'client',
})
.set('Content-Type', 'application/json')
@ -178,9 +175,9 @@ test('creates new client token with project & environment set', async () => {
return app.request
.post('/api/admin/api-tokens')
.send({
username: 'default-client',
tokenName: 'default-client',
type: 'client',
project: 'default',
projects: ['default'],
environment: DEFAULT_ENV,
})
.set('Content-Type', 'application/json')
@ -197,7 +194,7 @@ test('should prefix default token with "*:*."', async () => {
return app.request
.post('/api/admin/api-tokens')
.send({
username: 'default-client',
tokenName: 'default-client',
type: 'client',
})
.set('Content-Type', 'application/json')
@ -211,9 +208,9 @@ test('should prefix token with "project:environment."', async () => {
return app.request
.post('/api/admin/api-tokens')
.send({
username: 'default-client',
tokenName: 'default-client',
type: 'client',
project: 'default',
projects: ['default'],
environment: DEFAULT_ENV,
})
.set('Content-Type', 'application/json')
@ -227,9 +224,9 @@ test('should not create token for invalid projectId', async () => {
return app.request
.post('/api/admin/api-tokens')
.send({
username: 'default-client',
tokenName: 'default-client',
type: 'client',
project: 'bogus-project-something',
projects: ['bogus-project-something'],
})
.set('Content-Type', 'application/json')
.expect(400)
@ -244,7 +241,7 @@ test('should not create token for invalid environment', async () => {
return app.request
.post('/api/admin/api-tokens')
.send({
username: 'default-client',
tokenName: 'default-client',
type: 'client',
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
.post('/api/admin/api-tokens')
.send({
type: 'admin',
type: 'client',
environment: '*',
})
.set('Content-Type', 'application/json')
.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
.post('/api/admin/api-tokens')
.send({
username: 'default-client-name',
tokenName: 'default-token-name',
type: 'admin',
environment: '*',
@ -285,7 +281,7 @@ test('client tokens cannot span all environments', async () => {
return app.request
.post('/api/admin/api-tokens')
.send({
username: 'default-client',
tokenName: 'default-client',
type: 'client',
environment: ALL,
})
@ -302,9 +298,9 @@ test('should create token for disabled environment', async () => {
return app.request
.post('/api/admin/api-tokens')
.send({
username: 'default',
tokenName: 'default',
type: 'client',
project: 'default',
projects: ['default'],
environment: 'disabledEnvironment',
})
.set('Content-Type', 'application/json')

View File

@ -77,7 +77,7 @@ test('fails to create new client token when given wrong project', async () => {
return app.request
.post('/api/admin/projects/wrong/api-tokens')
.send({
username: 'default-client',
tokenName: 'default-client',
type: 'client',
projects: ['wrong'],
environment: 'default',
@ -90,7 +90,7 @@ test('creates new client token', async () => {
return app.request
.post('/api/admin/projects/default/api-tokens')
.send({
username: 'default-client',
tokenName: 'default-client',
type: 'client',
projects: ['default'],
environment: 'default',
@ -98,7 +98,7 @@ test('creates new client token', async () => {
.set('Content-Type', 'application/json')
.expect(201)
.expect((res) => {
expect(res.body.username).toBe('default-client');
expect(res.body.tokenName).toBe('default-client');
});
});

View File

@ -125,11 +125,11 @@ afterAll(async () => {
});
test('returns feature flag with "default" config', async () => {
const token = await apiTokenService.createApiToken({
const token = await apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT,
tokenName,
environment: DEFAULT_ENV,
project,
projects: [project],
});
await app.request
.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 () => {
const token = await apiTokenService.createApiToken({
const token = await apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT,
tokenName: tokenName,
environment,
project,
projects: [project],
});
await app.request
.get('/api/client/features')
@ -173,11 +173,11 @@ test('returns feature flag with testing environment config', async () => {
});
test('returns feature flag for project2', async () => {
const token = await apiTokenService.createApiToken({
const token = await apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT,
tokenName: tokenName,
environment,
project: project2,
projects: [project2],
});
await app.request
.get('/api/client/features')
@ -193,11 +193,11 @@ test('returns feature flag for project2', async () => {
});
test('returns feature flag for all projects', async () => {
const token = await apiTokenService.createApiToken({
const token = await apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT,
tokenName: tokenName,
environment,
project: '*',
projects: ['*'],
});
await app.request
.get('/api/client/features')

View File

@ -133,11 +133,11 @@ afterAll(async () => {
});
test('doesnt return feature flags if project deleted', async () => {
const token = await apiTokenService.createApiToken({
const token = await apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT,
tokenName: deletionTokenName,
environment,
project: deletionProject,
projects: [deletionProject],
});
await app.services.projectService.deleteProject(

View File

@ -31,11 +31,11 @@ test('should enrich metrics with environment from api-token', async () => {
type: 'test',
});
const token = await apiTokenService.createApiToken({
const token = await apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT,
tokenName: 'test',
environment: 'some',
project: '*',
projects: ['*'],
});
const featureName = Object.keys(metricsExample.bucket.toggles)[0];

View File

@ -74,12 +74,14 @@ test('should accept client metrics', async () => {
test('should pick up environment from token', async () => {
const environment = 'test';
await db.stores.environmentStore.create({ name: 'test', type: 'test' });
const token = await app.services.apiTokenService.createApiToken({
type: ApiTokenType.CLIENT,
project: 'default',
environment,
tokenName: 'tester',
});
const token = await app.services.apiTokenService.createApiTokenWithProjects(
{
type: ApiTokenType.CLIENT,
projects: ['default'],
environment,
tokenName: 'tester',
},
);
// @ts-expect-error - cachedFeatureNames is a private property in ClientMetricsServiceV2
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[]>>()
.mockResolvedValue(['t1', 't2']);
const token = await app.services.apiTokenService.createApiToken({
type: ApiTokenType.CLIENT,
project: 'default',
environment: 'default',
tokenName: 'tester',
});
const token = await app.services.apiTokenService.createApiTokenWithProjects(
{
type: ApiTokenType.CLIENT,
projects: ['default'],
environment: 'default',
tokenName: 'tester',
},
);
await app.request
.post('/api/client/metrics')

View File

@ -68,10 +68,10 @@ test('should have empty list of tokens', async () => {
});
test('should create client token', async () => {
const token = await apiTokenService.createApiToken({
const token = await apiTokenService.createApiTokenWithProjects({
tokenName: 'default-client',
type: ApiTokenType.CLIENT,
project: '*',
projects: ['*'],
environment: DEFAULT_ENV,
});
const allTokens = await apiTokenService.getAllTokens();
@ -79,15 +79,15 @@ test('should create client token', async () => {
expect(allTokens.length).toBe(1);
expect(token.secret.length > 32).toBe(true);
expect(token.type).toBe(ApiTokenType.CLIENT);
expect(token.username).toBe('default-client');
expect(token.tokenName).toBe('default-client');
expect(allTokens[0].secret).toBe(token.secret);
});
test('should create admin token', async () => {
const token = await apiTokenService.createApiToken({
const token = await apiTokenService.createApiTokenWithProjects({
tokenName: 'admin',
type: ApiTokenType.ADMIN,
project: '*',
projects: ['*'],
environment: '*',
});
@ -97,11 +97,11 @@ test('should create admin token', async () => {
test('should set expiry of token', async () => {
const time = new Date('2022-01-01');
await apiTokenService.createApiToken({
await apiTokenService.createApiTokenWithProjects({
tokenName: 'default-client',
type: ApiTokenType.CLIENT,
expiresAt: time,
project: '*',
projects: ['*'],
environment: DEFAULT_ENV,
});
@ -114,12 +114,12 @@ test('should update expiry of token', async () => {
const time = new Date('2022-01-01');
const newTime = new Date('2023-01-01');
const token = await apiTokenService.createApiToken(
const token = await apiTokenService.createApiTokenWithProjects(
{
tokenName: 'default-client',
type: ApiTokenType.CLIENT,
expiresAt: time,
project: '*',
projects: ['*'],
environment: DEFAULT_ENV,
},
TEST_AUDIT_USER,
@ -133,7 +133,7 @@ test('should update expiry of token', async () => {
});
test('should create client token with project list', async () => {
const token = await apiTokenService.createApiToken({
const token = await apiTokenService.createApiTokenWithProjects({
tokenName: 'default-client',
type: ApiTokenType.CLIENT,
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 () => {
const token = await apiTokenService.createApiToken({
const token = await apiTokenService.createApiTokenWithProjects({
tokenName: 'default-client',
type: ApiTokenType.CLIENT,
projects: ['*', 'default'],
@ -159,21 +159,23 @@ test('should return user with multiple projects', async () => {
const now = Date.now();
const tomorrow = addDays(now, 1);
const { secret: secret1 } = await apiTokenService.createApiToken({
tokenName: 'default-valid',
type: ApiTokenType.CLIENT,
expiresAt: tomorrow,
projects: ['test-project', 'default'],
environment: DEFAULT_ENV,
});
const { secret: secret1 } =
await apiTokenService.createApiTokenWithProjects({
tokenName: 'default-valid',
type: ApiTokenType.CLIENT,
expiresAt: tomorrow,
projects: ['test-project', 'default'],
environment: DEFAULT_ENV,
});
const { secret: secret2 } = await apiTokenService.createApiToken({
tokenName: 'default-also-valid',
type: ApiTokenType.CLIENT,
expiresAt: tomorrow,
projects: ['test-project'],
environment: DEFAULT_ENV,
});
const { secret: secret2 } =
await apiTokenService.createApiTokenWithProjects({
tokenName: 'default-also-valid',
type: ApiTokenType.CLIENT,
expiresAt: tomorrow,
projects: ['test-project'],
environment: DEFAULT_ENV,
});
const multiProjectUser = await apiTokenService.getUserForToken(secret1);
const singleProjectUser = await apiTokenService.getUserForToken(secret2);