mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: make edge use token's cache (#6893)
## About the changes This PR removes the feature flag `queryMissingTokens` that was fully rolled out. It introduces a new way of checking edgeValidTokens controlled by the flag `checkEdgeValidTokensFromCache` that relies in the cached data but hits the DB if needed. The assumption is that most of the times edge will find tokens in the cache, except for a few cases in which a new token is queried. From all tokens we expect at most one to hit the DB and in this case querying a single token should be better than querying all the tokens.
This commit is contained in:
		
							parent
							
								
									ff6297d338
								
							
						
					
					
						commit
						126b78896e
					
				@ -194,10 +194,12 @@ export class ApiTokenStore implements IApiTokenStore {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async get(key: string): Promise<IApiToken> {
 | 
					    async get(key: string): Promise<IApiToken> {
 | 
				
			||||||
 | 
					        const stopTimer = this.timer('get-by-secret');
 | 
				
			||||||
        const row = await this.makeTokenProjectQuery().where(
 | 
					        const row = await this.makeTokenProjectQuery().where(
 | 
				
			||||||
            'tokens.secret',
 | 
					            'tokens.secret',
 | 
				
			||||||
            key,
 | 
					            key,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					        stopTimer();
 | 
				
			||||||
        return toTokens(row)[0];
 | 
					        return toTokens(row)[0];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -49,7 +49,7 @@ export const scheduleServices = async (
 | 
				
			|||||||
        apiTokenService.fetchActiveTokens.bind(apiTokenService),
 | 
					        apiTokenService.fetchActiveTokens.bind(apiTokenService),
 | 
				
			||||||
        minutesToMilliseconds(1),
 | 
					        minutesToMilliseconds(1),
 | 
				
			||||||
        'fetchActiveTokens',
 | 
					        'fetchActiveTokens',
 | 
				
			||||||
        0,
 | 
					        0, // no jitter, we need tokens at startup
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    schedulerService.schedule(
 | 
					    schedulerService.schedule(
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,6 @@ import { ApiTokenType } from '../types/models/api-token';
 | 
				
			|||||||
import type { IUnleashConfig } from '../types/option';
 | 
					import type { IUnleashConfig } from '../types/option';
 | 
				
			||||||
import type { IApiRequest, IAuthRequest } from '../routes/unleash-types';
 | 
					import type { IApiRequest, IAuthRequest } from '../routes/unleash-types';
 | 
				
			||||||
import type { IUnleashServices } from '../server-impl';
 | 
					import type { IUnleashServices } from '../server-impl';
 | 
				
			||||||
import type { IFlagContext } from '../types';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isClientApi = ({ path }) => {
 | 
					const isClientApi = ({ path }) => {
 | 
				
			||||||
    return path && path.indexOf('/api/client') > -1;
 | 
					    return path && path.indexOf('/api/client') > -1;
 | 
				
			||||||
@ -27,20 +26,6 @@ const isProxyApi = ({ path }) => {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const contextFrom = (
 | 
					 | 
				
			||||||
    req: IAuthRequest<any, any, any, any> | IApiRequest<any, any, any, any>,
 | 
					 | 
				
			||||||
): IFlagContext | undefined => {
 | 
					 | 
				
			||||||
    // this is what we'd get from edge:
 | 
					 | 
				
			||||||
    // req_path: '/api/client/features',
 | 
					 | 
				
			||||||
    // req_user_agent: 'unleash-edge-16.0.4'
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
        reqPath: req.path,
 | 
					 | 
				
			||||||
        reqUserAgent: req.get ? req.get('User-Agent') ?? '' : '',
 | 
					 | 
				
			||||||
        reqAppName:
 | 
					 | 
				
			||||||
            req.headers?.['unleash-appname'] ?? req.query?.appName ?? '',
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const TOKEN_TYPE_ERROR_MESSAGE =
 | 
					export const TOKEN_TYPE_ERROR_MESSAGE =
 | 
				
			||||||
    'invalid token: expected a different token type for this endpoint';
 | 
					    'invalid token: expected a different token type for this endpoint';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -70,10 +55,7 @@ const apiAccessMiddleware = (
 | 
				
			|||||||
            const apiToken = req.header('authorization');
 | 
					            const apiToken = req.header('authorization');
 | 
				
			||||||
            if (!apiToken?.startsWith('user:')) {
 | 
					            if (!apiToken?.startsWith('user:')) {
 | 
				
			||||||
                const apiUser = apiToken
 | 
					                const apiUser = apiToken
 | 
				
			||||||
                    ? await apiTokenService.getUserForToken(
 | 
					                    ? await apiTokenService.getUserForToken(apiToken)
 | 
				
			||||||
                          apiToken,
 | 
					 | 
				
			||||||
                          contextFrom(req),
 | 
					 | 
				
			||||||
                      )
 | 
					 | 
				
			||||||
                    : undefined;
 | 
					                    : undefined;
 | 
				
			||||||
                const { CLIENT, FRONTEND } = ApiTokenType;
 | 
					                const { CLIENT, FRONTEND } = ApiTokenType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -197,17 +197,17 @@ test('getUserForToken should get a user with admin token user id and token name'
 | 
				
			|||||||
    expect(user!.internalAdminTokenUserId).toBe(ADMIN_TOKEN_USER.id);
 | 
					    expect(user!.internalAdminTokenUserId).toBe(ADMIN_TOKEN_USER.id);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('When token is added by another instance', () => {
 | 
					describe('API token getTokenWithCache', () => {
 | 
				
			||||||
    const setup = (options?: IUnleashOptions) => {
 | 
					    const token: IApiTokenCreate = {
 | 
				
			||||||
        const token: IApiTokenCreate = {
 | 
					        environment: 'default',
 | 
				
			||||||
            environment: 'default',
 | 
					        projects: ['*'],
 | 
				
			||||||
            projects: ['*'],
 | 
					        secret: '*:*:some-random-string',
 | 
				
			||||||
            secret: '*:*:some-random-string',
 | 
					        type: ApiTokenType.CLIENT,
 | 
				
			||||||
            type: ApiTokenType.CLIENT,
 | 
					        tokenName: 'new-token-by-another-instance',
 | 
				
			||||||
            tokenName: 'new-token-by-another-instance',
 | 
					        expiresAt: undefined,
 | 
				
			||||||
            expiresAt: undefined,
 | 
					    };
 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const setup = (options?: IUnleashOptions) => {
 | 
				
			||||||
        const config: IUnleashConfig = createTestConfig(options);
 | 
					        const config: IUnleashConfig = createTestConfig(options);
 | 
				
			||||||
        const apiTokenStore = new FakeApiTokenStore();
 | 
					        const apiTokenStore = new FakeApiTokenStore();
 | 
				
			||||||
        const environmentStore = new FakeEnvironmentStore();
 | 
					        const environmentStore = new FakeEnvironmentStore();
 | 
				
			||||||
@ -220,60 +220,43 @@ describe('When token is added by another instance', () => {
 | 
				
			|||||||
        return {
 | 
					        return {
 | 
				
			||||||
            apiTokenService,
 | 
					            apiTokenService,
 | 
				
			||||||
            apiTokenStore,
 | 
					            apiTokenStore,
 | 
				
			||||||
            token,
 | 
					 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    test('should not return the token when query db flag is disabled', async () => {
 | 
					 | 
				
			||||||
        const { apiTokenService, apiTokenStore, token } = setup();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // simulate this token being inserted by another instance (apiTokenService does not know about it)
 | 
					    test('should return the token and perform only one db query', async () => {
 | 
				
			||||||
 | 
					        const { apiTokenService, apiTokenStore } = setup();
 | 
				
			||||||
 | 
					        const apiTokenStoreGet = jest.spyOn(apiTokenStore, 'get');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // valid token not present in cache (could be inserted by another instance)
 | 
				
			||||||
        apiTokenStore.insert(token);
 | 
					        apiTokenStore.insert(token);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const found = await apiTokenService.getUserForToken(token.secret);
 | 
					        for (let i = 0; i < 5; i++) {
 | 
				
			||||||
        expect(found).toBeUndefined();
 | 
					            const found = await apiTokenService.getTokenWithCache(token.secret);
 | 
				
			||||||
    });
 | 
					            expect(found).toBeDefined();
 | 
				
			||||||
 | 
					            expect(found?.tokenName).toBe(token.tokenName);
 | 
				
			||||||
    test('should return the token when query db flag is enabled', async () => {
 | 
					            expect(found?.createdAt).toBeDefined();
 | 
				
			||||||
        const { apiTokenService, apiTokenStore, token } = setup({
 | 
					        }
 | 
				
			||||||
            experimental: {
 | 
					        expect(apiTokenStoreGet).toHaveBeenCalledTimes(1);
 | 
				
			||||||
                flags: {
 | 
					 | 
				
			||||||
                    queryMissingTokens: true,
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // simulate this token being inserted by another instance (apiTokenService does not know about it)
 | 
					 | 
				
			||||||
        apiTokenStore.insert(token);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const found = await apiTokenService.getUserForToken(token.secret);
 | 
					 | 
				
			||||||
        expect(found).toBeDefined();
 | 
					 | 
				
			||||||
        expect(found?.username).toBe(token.tokenName);
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    test('should query the db only once for invalid tokens', async () => {
 | 
					    test('should query the db only once for invalid tokens', async () => {
 | 
				
			||||||
        jest.useFakeTimers();
 | 
					        jest.useFakeTimers();
 | 
				
			||||||
        const { apiTokenService, apiTokenStore } = setup({
 | 
					        const { apiTokenService, apiTokenStore } = setup();
 | 
				
			||||||
            experimental: {
 | 
					 | 
				
			||||||
                flags: {
 | 
					 | 
				
			||||||
                    queryMissingTokens: true,
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        const apiTokenStoreGet = jest.spyOn(apiTokenStore, 'get');
 | 
					        const apiTokenStoreGet = jest.spyOn(apiTokenStore, 'get');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const invalidToken = 'invalid-token';
 | 
					        const invalidToken = 'invalid-token';
 | 
				
			||||||
        for (let i = 0; i < 10; i++) {
 | 
					        for (let i = 0; i < 5; i++) {
 | 
				
			||||||
            expect(
 | 
					            expect(
 | 
				
			||||||
                await apiTokenService.getUserForToken(invalidToken),
 | 
					                await apiTokenService.getTokenWithCache(invalidToken),
 | 
				
			||||||
            ).toBeUndefined();
 | 
					            ).toBeUndefined();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        expect(apiTokenStoreGet).toHaveBeenCalledTimes(1);
 | 
					        expect(apiTokenStoreGet).toHaveBeenCalledTimes(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // after more than 5 minutes we should be able to query again
 | 
					        // after more than 5 minutes we should be able to query again
 | 
				
			||||||
        jest.advanceTimersByTime(minutesToMilliseconds(6));
 | 
					        jest.advanceTimersByTime(minutesToMilliseconds(6));
 | 
				
			||||||
        for (let i = 0; i < 10; i++) {
 | 
					        for (let i = 0; i < 5; i++) {
 | 
				
			||||||
            expect(
 | 
					            expect(
 | 
				
			||||||
                await apiTokenService.getUserForToken(invalidToken),
 | 
					                await apiTokenService.getTokenWithCache(invalidToken),
 | 
				
			||||||
            ).toBeUndefined();
 | 
					            ).toBeUndefined();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        expect(apiTokenStoreGet).toHaveBeenCalledTimes(2);
 | 
					        expect(apiTokenStoreGet).toHaveBeenCalledTimes(2);
 | 
				
			||||||
 | 
				
			|||||||
@ -25,13 +25,14 @@ import {
 | 
				
			|||||||
    ApiTokenDeletedEvent,
 | 
					    ApiTokenDeletedEvent,
 | 
				
			||||||
    ApiTokenUpdatedEvent,
 | 
					    ApiTokenUpdatedEvent,
 | 
				
			||||||
    type IAuditUser,
 | 
					    type IAuditUser,
 | 
				
			||||||
    type IFlagContext,
 | 
					 | 
				
			||||||
    type IFlagResolver,
 | 
					    type IFlagResolver,
 | 
				
			||||||
    SYSTEM_USER_AUDIT,
 | 
					    SYSTEM_USER_AUDIT,
 | 
				
			||||||
} from '../types';
 | 
					} from '../types';
 | 
				
			||||||
import { omitKeys } from '../util';
 | 
					import { omitKeys } from '../util';
 | 
				
			||||||
import type EventService from '../features/events/event-service';
 | 
					import type EventService from '../features/events/event-service';
 | 
				
			||||||
import { addMinutes, isPast } from 'date-fns';
 | 
					import { addMinutes, isPast } from 'date-fns';
 | 
				
			||||||
 | 
					import metricsHelper from '../util/metrics-helper';
 | 
				
			||||||
 | 
					import { FUNCTION_TIME } from '../metric-events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const resolveTokenPermissions = (tokenType: string) => {
 | 
					const resolveTokenPermissions = (tokenType: string) => {
 | 
				
			||||||
    if (tokenType === ApiTokenType.ADMIN) {
 | 
					    if (tokenType === ApiTokenType.ADMIN) {
 | 
				
			||||||
@ -60,14 +61,14 @@ export class ApiTokenService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    private queryAfter = new Map<string, Date>();
 | 
					    private queryAfter = new Map<string, Date>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private initialized = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private eventService: EventService;
 | 
					    private eventService: EventService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private lastSeenSecrets: Set<string> = new Set<string>();
 | 
					    private lastSeenSecrets: Set<string> = new Set<string>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private flagResolver: IFlagResolver;
 | 
					    private flagResolver: IFlagResolver;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private timer: Function;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(
 | 
					    constructor(
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            apiTokenStore,
 | 
					            apiTokenStore,
 | 
				
			||||||
@ -75,7 +76,7 @@ export class ApiTokenService {
 | 
				
			|||||||
        }: Pick<IUnleashStores, 'apiTokenStore' | 'environmentStore'>,
 | 
					        }: Pick<IUnleashStores, 'apiTokenStore' | 'environmentStore'>,
 | 
				
			||||||
        config: Pick<
 | 
					        config: Pick<
 | 
				
			||||||
            IUnleashConfig,
 | 
					            IUnleashConfig,
 | 
				
			||||||
            'getLogger' | 'authentication' | 'flagResolver'
 | 
					            'getLogger' | 'authentication' | 'flagResolver' | 'eventBus'
 | 
				
			||||||
        >,
 | 
					        >,
 | 
				
			||||||
        eventService: EventService,
 | 
					        eventService: EventService,
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
@ -94,18 +95,21 @@ export class ApiTokenService {
 | 
				
			|||||||
                this.initApiTokens(config.authentication.initApiTokens),
 | 
					                this.initApiTokens(config.authentication.initApiTokens),
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        this.timer = (functionName: string) =>
 | 
				
			||||||
 | 
					            metricsHelper.wrapTimer(config.eventBus, FUNCTION_TIME, {
 | 
				
			||||||
 | 
					                className: 'ApiTokenService',
 | 
				
			||||||
 | 
					                functionName,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Executed by a scheduler to refresh all active tokens
 | 
					     * Called by a scheduler without jitter to refresh all active tokens
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    async fetchActiveTokens(): Promise<void> {
 | 
					    async fetchActiveTokens(): Promise<void> {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            this.activeTokens = await this.store.getAllActive();
 | 
					            this.activeTokens = await this.store.getAllActive();
 | 
				
			||||||
            this.initialized = true;
 | 
					        } catch (e) {
 | 
				
			||||||
        } finally {
 | 
					            this.logger.warn('Failed to fetch active tokens', e);
 | 
				
			||||||
            // biome-ignore lint/correctness/noUnsafeFinally: We ignored this for eslint. Leaving this here for now, server-impl test fails without it
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -113,50 +117,7 @@ export class ApiTokenService {
 | 
				
			|||||||
        return this.store.get(secret);
 | 
					        return this.store.get(secret);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async updateLastSeen(): Promise<void> {
 | 
					    async getTokenWithCache(secret: string): Promise<IApiToken | undefined> {
 | 
				
			||||||
        if (this.lastSeenSecrets.size > 0) {
 | 
					 | 
				
			||||||
            const toStore = [...this.lastSeenSecrets];
 | 
					 | 
				
			||||||
            this.lastSeenSecrets = new Set<string>();
 | 
					 | 
				
			||||||
            await this.store.markSeenAt(toStore);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public async getAllTokens(): Promise<IApiToken[]> {
 | 
					 | 
				
			||||||
        return this.store.getAll();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public async getAllActiveTokens(): Promise<IApiToken[]> {
 | 
					 | 
				
			||||||
        if (this.flagResolver.isEnabled('useMemoizedActiveTokens')) {
 | 
					 | 
				
			||||||
            if (!this.initialized) {
 | 
					 | 
				
			||||||
                // unlikely this will happen but nice to have a fail safe
 | 
					 | 
				
			||||||
                this.logger.info('Fetching active tokens before initialized');
 | 
					 | 
				
			||||||
                await this.fetchActiveTokens();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            return this.activeTokens;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            return this.store.getAllActive();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private async initApiTokens(tokens: ILegacyApiTokenCreate[]) {
 | 
					 | 
				
			||||||
        const tokenCount = await this.store.count();
 | 
					 | 
				
			||||||
        if (tokenCount > 0) {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            const createAll = tokens
 | 
					 | 
				
			||||||
                .map(mapLegacyTokenWithSecret)
 | 
					 | 
				
			||||||
                .map((t) => this.insertNewApiToken(t, SYSTEM_USER_AUDIT));
 | 
					 | 
				
			||||||
            await Promise.all(createAll);
 | 
					 | 
				
			||||||
        } catch (e) {
 | 
					 | 
				
			||||||
            this.logger.error('Unable to create initial Admin API tokens');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public async getUserForToken(
 | 
					 | 
				
			||||||
        secret: string,
 | 
					 | 
				
			||||||
        flagContext?: IFlagContext, // temporarily added, expected from the middleware
 | 
					 | 
				
			||||||
    ): Promise<IApiUser | undefined> {
 | 
					 | 
				
			||||||
        if (!secret) {
 | 
					        if (!secret) {
 | 
				
			||||||
            return undefined;
 | 
					            return undefined;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -178,11 +139,7 @@ export class ApiTokenService {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const nextAllowedQuery = this.queryAfter.get(secret) ?? 0;
 | 
					        const nextAllowedQuery = this.queryAfter.get(secret) ?? 0;
 | 
				
			||||||
        if (
 | 
					        if (!token && isPast(nextAllowedQuery)) {
 | 
				
			||||||
            !token &&
 | 
					 | 
				
			||||||
            isPast(nextAllowedQuery) &&
 | 
					 | 
				
			||||||
            this.flagResolver.isEnabled('queryMissingTokens', flagContext)
 | 
					 | 
				
			||||||
        ) {
 | 
					 | 
				
			||||||
            if (this.queryAfter.size > 1000) {
 | 
					            if (this.queryAfter.size > 1000) {
 | 
				
			||||||
                // establish a max limit for queryAfter size to prevent memory leak
 | 
					                // establish a max limit for queryAfter size to prevent memory leak
 | 
				
			||||||
                this.queryAfter.clear();
 | 
					                this.queryAfter.clear();
 | 
				
			||||||
@ -190,12 +147,52 @@ export class ApiTokenService {
 | 
				
			|||||||
            // prevent querying the same invalid secret multiple times. Expire after 5 minutes
 | 
					            // prevent querying the same invalid secret multiple times. Expire after 5 minutes
 | 
				
			||||||
            this.queryAfter.set(secret, addMinutes(new Date(), 5));
 | 
					            this.queryAfter.set(secret, addMinutes(new Date(), 5));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const stopCacheTimer = this.timer('getTokenWithCache.query');
 | 
				
			||||||
            token = await this.store.get(secret);
 | 
					            token = await this.store.get(secret);
 | 
				
			||||||
            if (token) {
 | 
					            if (token) {
 | 
				
			||||||
                this.activeTokens.push(token);
 | 
					                this.activeTokens.push(token);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            stopCacheTimer();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return token;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async updateLastSeen(): Promise<void> {
 | 
				
			||||||
 | 
					        if (this.lastSeenSecrets.size > 0) {
 | 
				
			||||||
 | 
					            const toStore = [...this.lastSeenSecrets];
 | 
				
			||||||
 | 
					            this.lastSeenSecrets = new Set<string>();
 | 
				
			||||||
 | 
					            await this.store.markSeenAt(toStore);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async getAllTokens(): Promise<IApiToken[]> {
 | 
				
			||||||
 | 
					        return this.store.getAll();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async getAllActiveTokens(): Promise<IApiToken[]> {
 | 
				
			||||||
 | 
					        return this.store.getAllActive();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private async initApiTokens(tokens: ILegacyApiTokenCreate[]) {
 | 
				
			||||||
 | 
					        const tokenCount = await this.store.count();
 | 
				
			||||||
 | 
					        if (tokenCount > 0) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            const createAll = tokens
 | 
				
			||||||
 | 
					                .map(mapLegacyTokenWithSecret)
 | 
				
			||||||
 | 
					                .map((t) => this.insertNewApiToken(t, SYSTEM_USER_AUDIT));
 | 
				
			||||||
 | 
					            await Promise.all(createAll);
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					            this.logger.error('Unable to create initial Admin API tokens');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async getUserForToken(
 | 
				
			||||||
 | 
					        secret: string,
 | 
				
			||||||
 | 
					    ): Promise<IApiUser | undefined> {
 | 
				
			||||||
 | 
					        const token = await this.getTokenWithCache(secret);
 | 
				
			||||||
        if (token) {
 | 
					        if (token) {
 | 
				
			||||||
            this.lastSeenSecrets.add(token.secret);
 | 
					            this.lastSeenSecrets.add(token.secret);
 | 
				
			||||||
            const apiUser: IApiUser = new ApiUser({
 | 
					            const apiUser: IApiUser = new ApiUser({
 | 
				
			||||||
 | 
				
			|||||||
@ -1,39 +1,83 @@
 | 
				
			|||||||
import type { IUnleashConfig } from '../types';
 | 
					import type { IFlagResolver, IUnleashConfig } from '../types';
 | 
				
			||||||
import type { Logger } from '../logger';
 | 
					import type { Logger } from '../logger';
 | 
				
			||||||
import type { EdgeTokenSchema } from '../openapi/spec/edge-token-schema';
 | 
					import type { EdgeTokenSchema } from '../openapi/spec/edge-token-schema';
 | 
				
			||||||
import { constantTimeCompare } from '../util/constantTimeCompare';
 | 
					import { constantTimeCompare } from '../util/constantTimeCompare';
 | 
				
			||||||
import type { ValidatedEdgeTokensSchema } from '../openapi/spec/validated-edge-tokens-schema';
 | 
					import type { ValidatedEdgeTokensSchema } from '../openapi/spec/validated-edge-tokens-schema';
 | 
				
			||||||
import type { ApiTokenService } from './api-token-service';
 | 
					import type { ApiTokenService } from './api-token-service';
 | 
				
			||||||
 | 
					import metricsHelper from '../util/metrics-helper';
 | 
				
			||||||
 | 
					import { FUNCTION_TIME } from '../metric-events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class EdgeService {
 | 
					export default class EdgeService {
 | 
				
			||||||
    private logger: Logger;
 | 
					    private logger: Logger;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private apiTokenService: ApiTokenService;
 | 
					    private apiTokenService: ApiTokenService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private flagResolver: IFlagResolver;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private timer: Function;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(
 | 
					    constructor(
 | 
				
			||||||
        { apiTokenService }: { apiTokenService: ApiTokenService },
 | 
					        { apiTokenService }: { apiTokenService: ApiTokenService },
 | 
				
			||||||
        { getLogger }: Pick<IUnleashConfig, 'getLogger'>,
 | 
					        {
 | 
				
			||||||
 | 
					            getLogger,
 | 
				
			||||||
 | 
					            flagResolver,
 | 
				
			||||||
 | 
					            eventBus,
 | 
				
			||||||
 | 
					        }: Pick<IUnleashConfig, 'getLogger' | 'flagResolver' | 'eventBus'>,
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
        this.logger = getLogger('lib/services/edge-service.ts');
 | 
					        this.logger = getLogger('lib/services/edge-service.ts');
 | 
				
			||||||
        this.apiTokenService = apiTokenService;
 | 
					        this.apiTokenService = apiTokenService;
 | 
				
			||||||
 | 
					        this.flagResolver = flagResolver;
 | 
				
			||||||
 | 
					        this.timer = (functionName: string) =>
 | 
				
			||||||
 | 
					            metricsHelper.wrapTimer(eventBus, FUNCTION_TIME, {
 | 
				
			||||||
 | 
					                className: 'EdgeService',
 | 
				
			||||||
 | 
					                functionName,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getValidTokens(tokens: string[]): Promise<ValidatedEdgeTokensSchema> {
 | 
					    async getValidTokens(tokens: string[]): Promise<ValidatedEdgeTokensSchema> {
 | 
				
			||||||
        const activeTokens = await this.apiTokenService.getAllActiveTokens();
 | 
					        if (this.flagResolver.isEnabled('checkEdgeValidTokensFromCache')) {
 | 
				
			||||||
        const edgeTokens = tokens.reduce((result: EdgeTokenSchema[], token) => {
 | 
					            const stopTimer = this.timer('validateTokensWithCache');
 | 
				
			||||||
            const dbToken = activeTokens.find((activeToken) =>
 | 
					            // new behavior: use cached tokens when possible
 | 
				
			||||||
                constantTimeCompare(activeToken.secret, token),
 | 
					            // use the db to fetch the missing ones
 | 
				
			||||||
            );
 | 
					            // cache stores both missing and active so we don't hammer the db
 | 
				
			||||||
            if (dbToken) {
 | 
					            const validatedTokens: EdgeTokenSchema[] = [];
 | 
				
			||||||
                result.push({
 | 
					            for (const token of tokens) {
 | 
				
			||||||
                    token: token,
 | 
					                const found =
 | 
				
			||||||
                    type: dbToken.type,
 | 
					                    await this.apiTokenService.getTokenWithCache(token);
 | 
				
			||||||
                    projects: dbToken.projects,
 | 
					                if (found) {
 | 
				
			||||||
                });
 | 
					                    validatedTokens.push({
 | 
				
			||||||
 | 
					                        token: token,
 | 
				
			||||||
 | 
					                        type: found.type,
 | 
				
			||||||
 | 
					                        projects: found.projects,
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return result;
 | 
					            stopTimer();
 | 
				
			||||||
        }, []);
 | 
					            return { tokens: validatedTokens };
 | 
				
			||||||
        return { tokens: edgeTokens };
 | 
					        } else {
 | 
				
			||||||
 | 
					            // old behavior: go to the db to fetch all tokens and then filter in memory
 | 
				
			||||||
 | 
					            const stopTimer = this.timer('validateTokensWithoutCache');
 | 
				
			||||||
 | 
					            const activeTokens =
 | 
				
			||||||
 | 
					                await this.apiTokenService.getAllActiveTokens();
 | 
				
			||||||
 | 
					            const edgeTokens = tokens.reduce(
 | 
				
			||||||
 | 
					                (result: EdgeTokenSchema[], token) => {
 | 
				
			||||||
 | 
					                    const dbToken = activeTokens.find((activeToken) =>
 | 
				
			||||||
 | 
					                        constantTimeCompare(activeToken.secret, token),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    if (dbToken) {
 | 
				
			||||||
 | 
					                        result.push({
 | 
				
			||||||
 | 
					                            token: token,
 | 
				
			||||||
 | 
					                            type: dbToken.type,
 | 
				
			||||||
 | 
					                            projects: dbToken.projects,
 | 
				
			||||||
 | 
					                        });
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    return result;
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                [],
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            stopTimer();
 | 
				
			||||||
 | 
					            return { tokens: edgeTokens };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -43,6 +43,7 @@ export type IFlagKey =
 | 
				
			|||||||
    | 'displayTrafficDataUsage'
 | 
					    | 'displayTrafficDataUsage'
 | 
				
			||||||
    | 'useMemoizedActiveTokens'
 | 
					    | 'useMemoizedActiveTokens'
 | 
				
			||||||
    | 'queryMissingTokens'
 | 
					    | 'queryMissingTokens'
 | 
				
			||||||
 | 
					    | 'checkEdgeValidTokensFromCache'
 | 
				
			||||||
    | 'userAccessUIEnabled'
 | 
					    | 'userAccessUIEnabled'
 | 
				
			||||||
    | 'disableUpdateMaxRevisionId'
 | 
					    | 'disableUpdateMaxRevisionId'
 | 
				
			||||||
    | 'disablePublishUnannouncedEvents'
 | 
					    | 'disablePublishUnannouncedEvents'
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user