mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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