mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02:00
feat: admin token calls get an admin token user (#5924)
## About the changes
Whenever we get a call from an admin token we want to associate it with
the [admin token
user](4d42093a07/src/lib/types/core.ts (L34-L41)
).
This should give us the needed audit for this type of calls that
currently were lacking a user id (we only stored a string with the token
name in the event log).
We consciously decided not to use `id` as the property to prevent any
unforeseen side effects. The reason is that only `IUser` type has an id
and adding an id to `IApiUser` might lead to confusion.
This commit is contained in:
parent
6a5ce1f2a0
commit
ceaaf3d0f3
@ -42,7 +42,7 @@ import {
|
|||||||
getStandardResponses,
|
getStandardResponses,
|
||||||
} from '../../openapi/util/standard-responses';
|
} from '../../openapi/util/standard-responses';
|
||||||
import { ProxyService } from '../../services/proxy-service';
|
import { ProxyService } from '../../services/proxy-service';
|
||||||
import { extractUsername } from '../../util';
|
import { extractUserId, extractUsername } from '../../util';
|
||||||
import { OperationDeniedError } from '../../error';
|
import { OperationDeniedError } from '../../error';
|
||||||
|
|
||||||
interface TokenParam {
|
interface TokenParam {
|
||||||
@ -323,6 +323,7 @@ export class ApiTokenController extends Controller {
|
|||||||
const token = await this.apiTokenService.createApiToken(
|
const token = await this.apiTokenService.createApiToken(
|
||||||
createToken,
|
createToken,
|
||||||
extractUsername(req),
|
extractUsername(req),
|
||||||
|
extractUserId(req),
|
||||||
);
|
);
|
||||||
this.openApiService.respondWithValidation(
|
this.openApiService.respondWithValidation(
|
||||||
201,
|
201,
|
||||||
|
@ -27,7 +27,7 @@ import {
|
|||||||
ProjectService,
|
ProjectService,
|
||||||
ProxyService,
|
ProxyService,
|
||||||
} from '../../../services';
|
} from '../../../services';
|
||||||
import { extractUsername } from '../../../util';
|
import { extractUserId, extractUsername } from '../../../util';
|
||||||
import { IAuthRequest } from '../../unleash-types';
|
import { IAuthRequest } from '../../unleash-types';
|
||||||
import Controller from '../../controller';
|
import Controller from '../../controller';
|
||||||
import { Logger } from '../../../logger';
|
import { Logger } from '../../../logger';
|
||||||
@ -190,6 +190,7 @@ export class ProjectApiTokenController extends Controller {
|
|||||||
const token = await this.apiTokenService.createApiToken(
|
const token = await this.apiTokenService.createApiToken(
|
||||||
createToken,
|
createToken,
|
||||||
extractUsername(req),
|
extractUsername(req),
|
||||||
|
extractUserId(req),
|
||||||
);
|
);
|
||||||
this.openApiService.respondWithValidation(
|
this.openApiService.respondWithValidation(
|
||||||
201,
|
201,
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { ApiTokenService } from './api-token-service';
|
import { ApiTokenService } from './api-token-service';
|
||||||
import { createTestConfig } from '../../test/config/test-config';
|
import { createTestConfig } from '../../test/config/test-config';
|
||||||
import { IUnleashConfig } from '../server-impl';
|
import { IUnleashConfig, IUser } from '../server-impl';
|
||||||
import { ApiTokenType, IApiTokenCreate } from '../types/models/api-token';
|
import { ApiTokenType, IApiTokenCreate } from '../types/models/api-token';
|
||||||
import FakeApiTokenStore from '../../test/fixtures/fake-api-token-store';
|
import FakeApiTokenStore from '../../test/fixtures/fake-api-token-store';
|
||||||
import FakeEnvironmentStore from '../features/project-environments/fake-environment-store';
|
import FakeEnvironmentStore from '../features/project-environments/fake-environment-store';
|
||||||
import FakeEventStore from '../../test/fixtures/fake-event-store';
|
import FakeEventStore from '../../test/fixtures/fake-event-store';
|
||||||
import {
|
import {
|
||||||
|
ADMIN_TOKEN_USER,
|
||||||
API_TOKEN_CREATED,
|
API_TOKEN_CREATED,
|
||||||
API_TOKEN_DELETED,
|
API_TOKEN_DELETED,
|
||||||
API_TOKEN_UPDATED,
|
API_TOKEN_UPDATED,
|
||||||
@ -13,6 +14,7 @@ import {
|
|||||||
import { addDays } from 'date-fns';
|
import { addDays } from 'date-fns';
|
||||||
import EventService from './event-service';
|
import EventService from './event-service';
|
||||||
import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store';
|
import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store';
|
||||||
|
import { createFakeEventsService } from '../../lib/features';
|
||||||
|
|
||||||
test('Should init api token', async () => {
|
test('Should init api token', async () => {
|
||||||
const token = {
|
const token = {
|
||||||
@ -34,13 +36,7 @@ test('Should init api token', async () => {
|
|||||||
apiTokenStore.on('insert', resolve);
|
apiTokenStore.on('insert', resolve);
|
||||||
});
|
});
|
||||||
|
|
||||||
const eventService = new EventService(
|
const eventService = createFakeEventsService(config);
|
||||||
{
|
|
||||||
eventStore: new FakeEventStore(),
|
|
||||||
featureTagStore: new FakeFeatureTagStore(),
|
|
||||||
},
|
|
||||||
config,
|
|
||||||
);
|
|
||||||
|
|
||||||
new ApiTokenService(
|
new ApiTokenService(
|
||||||
{ apiTokenStore, environmentStore },
|
{ apiTokenStore, environmentStore },
|
||||||
@ -161,3 +157,31 @@ test('Api token operations should all have events attached', async () => {
|
|||||||
expect(deletedApiTokenEvents[0].preData).toBeDefined();
|
expect(deletedApiTokenEvents[0].preData).toBeDefined();
|
||||||
expect(deletedApiTokenEvents[0].preData.secret).toBeUndefined();
|
expect(deletedApiTokenEvents[0].preData.secret).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('getUserForToken should get a user with admin token user id and token name', async () => {
|
||||||
|
const config = createTestConfig();
|
||||||
|
const apiTokenStore = new FakeApiTokenStore();
|
||||||
|
const environmentStore = new FakeEnvironmentStore();
|
||||||
|
|
||||||
|
const eventService = createFakeEventsService(config);
|
||||||
|
|
||||||
|
const tokenService = new ApiTokenService(
|
||||||
|
{ apiTokenStore, environmentStore },
|
||||||
|
config,
|
||||||
|
eventService,
|
||||||
|
);
|
||||||
|
const token = await tokenService.createApiTokenWithProjects(
|
||||||
|
{
|
||||||
|
environment: '*',
|
||||||
|
projects: ['*'],
|
||||||
|
type: ApiTokenType.ADMIN,
|
||||||
|
tokenName: 'admin.token',
|
||||||
|
},
|
||||||
|
ADMIN_TOKEN_USER as IUser,
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = tokenService.getUserForToken(token.secret);
|
||||||
|
expect(user).toBeDefined();
|
||||||
|
expect(user!.username).toBe(token.tokenName);
|
||||||
|
expect(user!.internalAdminTokenUserId).toBe(ADMIN_TOKEN_USER.id);
|
||||||
|
});
|
||||||
|
@ -20,13 +20,19 @@ import BadDataError from '../error/bad-data-error';
|
|||||||
import { IEnvironmentStore } from '../features/project-environments/environment-store-type';
|
import { IEnvironmentStore } from '../features/project-environments/environment-store-type';
|
||||||
import { constantTimeCompare } from '../util/constantTimeCompare';
|
import { constantTimeCompare } from '../util/constantTimeCompare';
|
||||||
import {
|
import {
|
||||||
|
ADMIN_TOKEN_USER,
|
||||||
ApiTokenCreatedEvent,
|
ApiTokenCreatedEvent,
|
||||||
ApiTokenDeletedEvent,
|
ApiTokenDeletedEvent,
|
||||||
ApiTokenUpdatedEvent,
|
ApiTokenUpdatedEvent,
|
||||||
|
IUser,
|
||||||
SYSTEM_USER,
|
SYSTEM_USER,
|
||||||
SYSTEM_USER_ID,
|
SYSTEM_USER_ID,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { omitKeys } from '../util';
|
import {
|
||||||
|
extractUserIdFromUser,
|
||||||
|
extractUsernameFromUser,
|
||||||
|
omitKeys,
|
||||||
|
} from '../util';
|
||||||
import EventService from './event-service';
|
import EventService from './event-service';
|
||||||
|
|
||||||
const resolveTokenPermissions = (tokenType: string) => {
|
const resolveTokenPermissions = (tokenType: string) => {
|
||||||
@ -152,8 +158,7 @@ export class ApiTokenService {
|
|||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
this.lastSeenSecrets.add(token.secret);
|
this.lastSeenSecrets.add(token.secret);
|
||||||
|
const apiUser: IApiUser = new ApiUser({
|
||||||
return new ApiUser({
|
|
||||||
tokenName: token.tokenName,
|
tokenName: token.tokenName,
|
||||||
permissions: resolveTokenPermissions(token.type),
|
permissions: resolveTokenPermissions(token.type),
|
||||||
projects: token.projects,
|
projects: token.projects,
|
||||||
@ -161,6 +166,12 @@ export class ApiTokenService {
|
|||||||
type: token.type,
|
type: token.type,
|
||||||
secret: token.secret,
|
secret: token.secret,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
apiUser.internalAdminTokenUserId =
|
||||||
|
token.type === ApiTokenType.ADMIN
|
||||||
|
? ADMIN_TOKEN_USER.id
|
||||||
|
: undefined;
|
||||||
|
return apiUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -212,17 +223,46 @@ export class ApiTokenService {
|
|||||||
createdByUserId: number = SYSTEM_USER.id,
|
createdByUserId: number = SYSTEM_USER.id,
|
||||||
): Promise<IApiToken> {
|
): Promise<IApiToken> {
|
||||||
const token = mapLegacyToken(newToken);
|
const token = mapLegacyToken(newToken);
|
||||||
return this.createApiTokenWithProjects(
|
return this.internalCreateApiTokenWithProjects(
|
||||||
token,
|
token,
|
||||||
createdBy,
|
createdBy,
|
||||||
createdByUserId,
|
createdByUserId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param newToken
|
||||||
|
* @param createdBy should be IApiUser or IUser. Still supports optional or string for backward compatibility
|
||||||
|
* @param createdByUserId still supported for backward compatibility
|
||||||
|
*/
|
||||||
public async createApiTokenWithProjects(
|
public async createApiTokenWithProjects(
|
||||||
newToken: Omit<IApiTokenCreate, 'secret'>,
|
newToken: Omit<IApiTokenCreate, 'secret'>,
|
||||||
createdBy: string = SYSTEM_USER.username,
|
createdBy?: string | IApiUser | IUser,
|
||||||
createdByUserId: number = SYSTEM_USER.id,
|
createdByUserId?: number,
|
||||||
|
): Promise<IApiToken> {
|
||||||
|
// if statement to support old method signature
|
||||||
|
if (
|
||||||
|
createdBy === undefined ||
|
||||||
|
typeof createdBy === 'string' ||
|
||||||
|
createdByUserId
|
||||||
|
) {
|
||||||
|
return this.internalCreateApiTokenWithProjects(
|
||||||
|
newToken,
|
||||||
|
(createdBy as string) || SYSTEM_USER.username,
|
||||||
|
createdByUserId || SYSTEM_USER.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.internalCreateApiTokenWithProjects(
|
||||||
|
newToken,
|
||||||
|
extractUsernameFromUser(createdBy),
|
||||||
|
extractUserIdFromUser(createdBy),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async internalCreateApiTokenWithProjects(
|
||||||
|
newToken: Omit<IApiTokenCreate, 'secret'>,
|
||||||
|
createdBy: string,
|
||||||
|
createdByUserId: number,
|
||||||
): Promise<IApiToken> {
|
): Promise<IApiToken> {
|
||||||
validateApiToken(newToken);
|
validateApiToken(newToken);
|
||||||
const environments = await this.environmentStore.getAll();
|
const environments = await this.environmentStore.getAll();
|
||||||
|
20
src/lib/services/event-service.test.ts
Normal file
20
src/lib/services/event-service.test.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { ADMIN_TOKEN_USER, IApiUser } from '../types';
|
||||||
|
import { createTestConfig } from '../../test/config/test-config';
|
||||||
|
import { createFakeEventsService } from '../../lib/features';
|
||||||
|
import { ApiTokenType } from '../../lib/types/models/api-token';
|
||||||
|
|
||||||
|
test('when using an admin token should get the username of the token and the id from internalAdminTokenUserId', async () => {
|
||||||
|
const adminToken: IApiUser = {
|
||||||
|
projects: ['*'],
|
||||||
|
environment: '*',
|
||||||
|
type: ApiTokenType.ADMIN,
|
||||||
|
secret: '',
|
||||||
|
username: 'admin-token-username',
|
||||||
|
permissions: [],
|
||||||
|
internalAdminTokenUserId: ADMIN_TOKEN_USER.id,
|
||||||
|
};
|
||||||
|
const eventService = createFakeEventsService(createTestConfig());
|
||||||
|
const userDetails = eventService.getUserDetails(adminToken);
|
||||||
|
expect(userDetails.createdBy).toBe('admin-token-username');
|
||||||
|
expect(userDetails.createdByUserId).toBe(ADMIN_TOKEN_USER.id);
|
||||||
|
});
|
@ -2,10 +2,11 @@ import { IUnleashConfig } from '../types/option';
|
|||||||
import { IFeatureTagStore, IUnleashStores } from '../types/stores';
|
import { IFeatureTagStore, IUnleashStores } from '../types/stores';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import { IEventStore } from '../types/stores/event-store';
|
import { IEventStore } from '../types/stores/event-store';
|
||||||
import { IBaseEvent, IEventList } from '../types/events';
|
import { IBaseEvent, IEventList, IUserEvent } from '../types/events';
|
||||||
import { SearchEventsSchema } from '../openapi/spec/search-events-schema';
|
import { SearchEventsSchema } from '../openapi/spec/search-events-schema';
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import { ITag } from '../types';
|
import { ADMIN_TOKEN_USER, IApiUser, ITag, IUser, SYSTEM_USER } from '../types';
|
||||||
|
import { ApiTokenType } from '../../lib/types/models/api-token';
|
||||||
|
|
||||||
export default class EventService {
|
export default class EventService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
@ -81,10 +82,38 @@ export default class EventService {
|
|||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAdminToken(user: IUser | IApiUser): boolean {
|
||||||
|
return (user as IApiUser)?.type === ApiTokenType.ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserDetails(user: IUser | IApiUser): {
|
||||||
|
createdBy: string;
|
||||||
|
createdByUserId: number;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
createdBy:
|
||||||
|
(user as IUser)?.email ||
|
||||||
|
user?.username ||
|
||||||
|
(this.isAdminToken(user)
|
||||||
|
? ADMIN_TOKEN_USER.username
|
||||||
|
: SYSTEM_USER.username),
|
||||||
|
createdByUserId:
|
||||||
|
(user as IUser)?.id ||
|
||||||
|
(user as IApiUser)?.internalAdminTokenUserId ||
|
||||||
|
SYSTEM_USER.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use storeUserEvent instead
|
||||||
|
*/
|
||||||
async storeEvent(event: IBaseEvent): Promise<void> {
|
async storeEvent(event: IBaseEvent): Promise<void> {
|
||||||
return this.storeEvents([event]);
|
return this.storeEvents([event]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use storeUserEvents instead
|
||||||
|
*/
|
||||||
async storeEvents(events: IBaseEvent[]): Promise<void> {
|
async storeEvents(events: IBaseEvent[]): Promise<void> {
|
||||||
let enhancedEvents = events;
|
let enhancedEvents = events;
|
||||||
for (const enhancer of [this.enhanceEventsWithTags.bind(this)]) {
|
for (const enhancer of [this.enhanceEventsWithTags.bind(this)]) {
|
||||||
@ -92,4 +121,21 @@ export default class EventService {
|
|||||||
}
|
}
|
||||||
return this.eventStore.batchStore(enhancedEvents);
|
return this.eventStore.batchStore(enhancedEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async storeUserEvent(event: IUserEvent): Promise<void> {
|
||||||
|
return this.storeUserEvents([event]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeUserEvents(events: IUserEvent[]): Promise<void> {
|
||||||
|
let enhancedEvents = events.map(({ byUser, ...event }) => {
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
...this.getUserDetails(byUser),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
for (const enhancer of [this.enhanceEventsWithTags.bind(this)]) {
|
||||||
|
enhancedEvents = await enhancer(enhancedEvents);
|
||||||
|
}
|
||||||
|
return this.eventStore.batchStore(enhancedEvents);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ export interface IApiUserData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IApiUser {
|
export interface IApiUser {
|
||||||
|
internalAdminTokenUserId?: number; // user associated to an admin token
|
||||||
username: string;
|
username: string;
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
projects: string[];
|
projects: string[];
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { extractUsernameFromUser } from '../util';
|
import { extractUsernameFromUser } from '../util';
|
||||||
|
import { IApiUser } from './api-user';
|
||||||
import { FeatureToggle, IStrategyConfig, ITag, IVariant } from './model';
|
import { FeatureToggle, IStrategyConfig, ITag, IVariant } from './model';
|
||||||
import { IApiToken } from './models/api-token';
|
import { IApiToken } from './models/api-token';
|
||||||
import { IUser, IUserWithRootRole } from './user';
|
import { IUser, IUserWithRootRole } from './user';
|
||||||
@ -336,6 +337,9 @@ export const IEventTypes = [
|
|||||||
] as const;
|
] as const;
|
||||||
export type IEventType = (typeof IEventTypes)[number];
|
export type IEventType = (typeof IEventTypes)[number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This type should only be used in the store layer but deprecated elsewhere
|
||||||
|
*/
|
||||||
export interface IBaseEvent {
|
export interface IBaseEvent {
|
||||||
type: IEventType;
|
type: IEventType;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
@ -348,6 +352,17 @@ export interface IBaseEvent {
|
|||||||
tags?: ITag[];
|
tags?: ITag[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IUserEvent {
|
||||||
|
type: IEventType;
|
||||||
|
byUser: IUser | IApiUser;
|
||||||
|
project?: string;
|
||||||
|
environment?: string;
|
||||||
|
featureName?: string;
|
||||||
|
data?: any;
|
||||||
|
preData?: any;
|
||||||
|
tags?: ITag[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface IEvent extends IBaseEvent {
|
export interface IEvent extends IBaseEvent {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
import { SYSTEM_USER } from '../../lib/types';
|
||||||
import { IUser } from '../server-impl';
|
import { IUser } from '../server-impl';
|
||||||
import { extractUsernameFromUser } from './extract-user';
|
import { extractUserIdFromUser, extractUsernameFromUser } from './extract-user';
|
||||||
|
|
||||||
describe('extractUsernameFromUser', () => {
|
describe('extractUsernameFromUser', () => {
|
||||||
test('Should return the email if it exists', () => {
|
test('Should return the email if it exists', () => {
|
||||||
@ -19,14 +20,16 @@ describe('extractUsernameFromUser', () => {
|
|||||||
expect(extractUsernameFromUser(user)).toBe(user.username);
|
expect(extractUsernameFromUser(user)).toBe(user.username);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should return "unknown" if neither email nor username exists', () => {
|
test('Should return the system user if neither email nor username exists', () => {
|
||||||
const user = {} as IUser;
|
const user = {} as IUser;
|
||||||
|
|
||||||
expect(extractUsernameFromUser(user)).toBe('unknown');
|
expect(extractUsernameFromUser(user)).toBe(SYSTEM_USER.username);
|
||||||
|
expect(extractUserIdFromUser(user)).toBe(SYSTEM_USER.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should return "unknown" if user is null', () => {
|
test('Should return the system user if user is null', () => {
|
||||||
const user = null as unknown as IUser;
|
const user = null as unknown as IUser;
|
||||||
expect(extractUsernameFromUser(user)).toBe('unknown');
|
expect(extractUsernameFromUser(user)).toBe(SYSTEM_USER.username);
|
||||||
|
expect(extractUserIdFromUser(user)).toBe(SYSTEM_USER.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,16 +1,23 @@
|
|||||||
import { IAuthRequest, IUser } from '../server-impl';
|
import { SYSTEM_USER } from '../../lib/types';
|
||||||
|
import { IApiRequest, IApiUser, IAuthRequest, IUser } from '../server-impl';
|
||||||
|
|
||||||
export function extractUsernameFromUser(user: IUser): string {
|
export function extractUsernameFromUser(user: IUser | IApiUser): string {
|
||||||
return user?.email || user?.username || 'unknown';
|
return (user as IUser)?.email || user?.username || SYSTEM_USER.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractUsername(req: IAuthRequest): string {
|
export function extractUsername(req: IAuthRequest | IApiRequest): string {
|
||||||
return extractUsernameFromUser(req.user);
|
return extractUsernameFromUser(req.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const extractUserId = (req: IAuthRequest) => req.user.id;
|
export const extractUserIdFromUser = (user: IUser | IApiUser) =>
|
||||||
|
(user as IUser)?.id ||
|
||||||
|
(user as IApiUser)?.internalAdminTokenUserId ||
|
||||||
|
SYSTEM_USER.id;
|
||||||
|
|
||||||
export const extractUserInfo = (req: IAuthRequest) => ({
|
export const extractUserId = (req: IAuthRequest | IApiRequest) =>
|
||||||
|
extractUserIdFromUser(req.user);
|
||||||
|
|
||||||
|
export const extractUserInfo = (req: IAuthRequest | IApiRequest) => ({
|
||||||
id: extractUserId(req),
|
id: extractUserId(req),
|
||||||
username: extractUsername(req),
|
username: extractUsername(req),
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import { setupAppWithCustomAuth } from '../../helpers/test-helper';
|
import {
|
||||||
|
setupAppWithAuth,
|
||||||
|
setupAppWithCustomAuth,
|
||||||
|
} from '../../helpers/test-helper';
|
||||||
import dbInit, { ITestDb } from '../../helpers/database-init';
|
import dbInit, { ITestDb } from '../../helpers/database-init';
|
||||||
import getLogger from '../../../fixtures/no-logger';
|
import getLogger from '../../../fixtures/no-logger';
|
||||||
import { ApiTokenType } from '../../../../lib/types/models/api-token';
|
import { ApiTokenType } from '../../../../lib/types/models/api-token';
|
||||||
import { RoleName } from '../../../../lib/types/model';
|
import { RoleName } from '../../../../lib/types/model';
|
||||||
import {
|
import {
|
||||||
|
ADMIN_TOKEN_USER,
|
||||||
CREATE_CLIENT_API_TOKEN,
|
CREATE_CLIENT_API_TOKEN,
|
||||||
CREATE_PROJECT_API_TOKEN,
|
CREATE_PROJECT_API_TOKEN,
|
||||||
DELETE_CLIENT_API_TOKEN,
|
DELETE_CLIENT_API_TOKEN,
|
||||||
@ -34,6 +38,16 @@ afterEach(async () => {
|
|||||||
await stores.apiTokenStore.deleteAll();
|
await stores.apiTokenStore.deleteAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getLastEvent = async () => {
|
||||||
|
const events = await db.stores.eventStore.getEvents();
|
||||||
|
return events.reduce((last, current) => {
|
||||||
|
if (current.id > last.id) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
return last;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
test('editor users should only get client or frontend tokens', async () => {
|
test('editor users should only get client or frontend tokens', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
|
|
||||||
@ -190,6 +204,35 @@ test('Token-admin should be allowed to create token', async () => {
|
|||||||
await destroy();
|
await destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('An admin token should be allowed to create a token', async () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
|
const adminToken = await db.stores.apiTokenStore.insert({
|
||||||
|
type: ApiTokenType.ADMIN,
|
||||||
|
secret: '12345',
|
||||||
|
environment: '',
|
||||||
|
projects: [],
|
||||||
|
tokenName: 'default-admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { request, destroy } = await setupAppWithAuth(stores);
|
||||||
|
|
||||||
|
await request
|
||||||
|
.post('/api/admin/api-tokens')
|
||||||
|
.send({
|
||||||
|
username: 'default-admin',
|
||||||
|
type: 'admin',
|
||||||
|
})
|
||||||
|
.set('Authorization', adminToken.secret)
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const event = await getLastEvent();
|
||||||
|
expect(event.createdBy).toBe(adminToken.tokenName);
|
||||||
|
expect(event.createdByUserId).toBe(ADMIN_TOKEN_USER.id);
|
||||||
|
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);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user