mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	chore: edge active tokens cache flag removal (#7094)
## About the changes EdgeService is the only place where we use active tokens validation in bulk. By switching to validating from the cache, we no longer need a method to return all active tokens from the DB.
This commit is contained in:
		
							parent
							
								
									6a1b6fd024
								
							
						
					
					
						commit
						8ac8d873b4
					
				| @ -142,15 +142,6 @@ export class PublicSignupTokenStore implements IPublicSignupTokenStore { | ||||
|         return toTokens(rows); | ||||
|     } | ||||
| 
 | ||||
|     async getAllActive(): Promise<PublicSignupTokenSchema[]> { | ||||
|         const stopTimer = this.timer('getAllActive'); | ||||
|         const rows = await this.makeTokenUsersQuery() | ||||
|             .where('expires_at', 'IS', null) | ||||
|             .orWhere('expires_at', '>', 'now()'); | ||||
|         stopTimer(); | ||||
|         return toTokens(rows); | ||||
|     } | ||||
| 
 | ||||
|     async addTokenUser(secret: string, userId: number): Promise<void> { | ||||
|         await this.db<ITokenUserRow>(TOKEN_USERS_TABLE).insert( | ||||
|             { user_id: userId, secret }, | ||||
|  | ||||
| @ -185,10 +185,6 @@ export class ApiTokenService { | ||||
|         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) { | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| import type { IFlagResolver, IUnleashConfig } from '../types'; | ||||
| import type { IUnleashConfig } from '../types'; | ||||
| import type { Logger } from '../logger'; | ||||
| import type { EdgeTokenSchema } from '../openapi/spec/edge-token-schema'; | ||||
| import { constantTimeCompare } from '../util/constantTimeCompare'; | ||||
| import type { ValidatedEdgeTokensSchema } from '../openapi/spec/validated-edge-tokens-schema'; | ||||
| import type { ApiTokenService } from './api-token-service'; | ||||
| import metricsHelper from '../util/metrics-helper'; | ||||
| @ -12,21 +11,17 @@ export default class EdgeService { | ||||
| 
 | ||||
|     private apiTokenService: ApiTokenService; | ||||
| 
 | ||||
|     private flagResolver: IFlagResolver; | ||||
| 
 | ||||
|     private timer: Function; | ||||
| 
 | ||||
|     constructor( | ||||
|         { apiTokenService }: { apiTokenService: ApiTokenService }, | ||||
|         { | ||||
|             getLogger, | ||||
|             flagResolver, | ||||
|             eventBus, | ||||
|         }: Pick<IUnleashConfig, 'getLogger' | 'flagResolver' | 'eventBus'>, | ||||
|     ) { | ||||
|         this.logger = getLogger('lib/services/edge-service.ts'); | ||||
|         this.apiTokenService = apiTokenService; | ||||
|         this.flagResolver = flagResolver; | ||||
|         this.timer = (functionName: string) => | ||||
|             metricsHelper.wrapTimer(eventBus, FUNCTION_TIME, { | ||||
|                 className: 'EdgeService', | ||||
| @ -35,49 +30,23 @@ export default class EdgeService { | ||||
|     } | ||||
| 
 | ||||
|     async getValidTokens(tokens: string[]): Promise<ValidatedEdgeTokensSchema> { | ||||
|         if (this.flagResolver.isEnabled('checkEdgeValidTokensFromCache')) { | ||||
|             const stopTimer = this.timer('validateTokensWithCache'); | ||||
|             // new behavior: use cached tokens when possible
 | ||||
|             // use the db to fetch the missing ones
 | ||||
|             // cache stores both missing and active so we don't hammer the db
 | ||||
|             const validatedTokens: EdgeTokenSchema[] = []; | ||||
|             for (const token of tokens) { | ||||
|                 const found = | ||||
|                     await this.apiTokenService.getTokenWithCache(token); | ||||
|                 if (found) { | ||||
|                     validatedTokens.push({ | ||||
|                         token: token, | ||||
|                         type: found.type, | ||||
|                         projects: found.projects, | ||||
|                     }); | ||||
|                 } | ||||
|         const stopTimer = this.timer('validateTokensWithCache'); | ||||
|         // new behavior: use cached tokens when possible
 | ||||
|         // use the db to fetch the missing ones
 | ||||
|         // cache stores both missing and active so we don't hammer the db
 | ||||
|         const validatedTokens: EdgeTokenSchema[] = []; | ||||
|         for (const token of tokens) { | ||||
|             const found = await this.apiTokenService.getTokenWithCache(token); | ||||
|             if (found) { | ||||
|                 validatedTokens.push({ | ||||
|                     token: token, | ||||
|                     type: found.type, | ||||
|                     projects: found.projects, | ||||
|                 }); | ||||
|             } | ||||
|             stopTimer(); | ||||
|             return { tokens: validatedTokens }; | ||||
|         } 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 }; | ||||
|         } | ||||
|         stopTimer(); | ||||
|         return { tokens: validatedTokens }; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -70,10 +70,6 @@ export class PublicSignupTokenService { | ||||
|         return this.store.getAll(); | ||||
|     } | ||||
| 
 | ||||
|     public async getAllActiveTokens(): Promise<PublicSignupTokenSchema[]> { | ||||
|         return this.store.getAllActive(); | ||||
|     } | ||||
| 
 | ||||
|     public async validate(secret: string): Promise<boolean> { | ||||
|         return this.store.isValid(secret); | ||||
|     } | ||||
|  | ||||
| @ -4,7 +4,6 @@ import type { IPublicSignupTokenCreate } from '../models/public-signup-token'; | ||||
| 
 | ||||
| export interface IPublicSignupTokenStore | ||||
|     extends Store<PublicSignupTokenSchema, string> { | ||||
|     getAllActive(): Promise<PublicSignupTokenSchema[]>; | ||||
|     insert( | ||||
|         newToken: IPublicSignupTokenCreate, | ||||
|     ): Promise<PublicSignupTokenSchema>; | ||||
|  | ||||
| @ -7,7 +7,7 @@ import { | ||||
|     type IApiToken, | ||||
| } from '../../../lib/types/models/api-token'; | ||||
| import { DEFAULT_ENV } from '../../../lib/util/constants'; | ||||
| import { addDays, subDays } from 'date-fns'; | ||||
| import { addDays } from 'date-fns'; | ||||
| import type ProjectService from '../../../lib/features/project/project-service'; | ||||
| import { createProjectService } from '../../../lib/features'; | ||||
| import { EventService } from '../../../lib/services'; | ||||
| @ -133,33 +133,6 @@ test('should update expiry of token', async () => { | ||||
|     expect(updatedToken.expiresAt).toEqual(newTime); | ||||
| }); | ||||
| 
 | ||||
| test('should only return valid tokens', async () => { | ||||
|     const now = Date.now(); | ||||
|     const yesterday = subDays(now, 1); | ||||
|     const tomorrow = addDays(now, 1); | ||||
| 
 | ||||
|     await apiTokenService.createApiToken({ | ||||
|         tokenName: 'default-expired', | ||||
|         type: ApiTokenType.CLIENT, | ||||
|         expiresAt: yesterday, | ||||
|         project: '*', | ||||
|         environment: DEFAULT_ENV, | ||||
|     }); | ||||
| 
 | ||||
|     const activeToken = await apiTokenService.createApiToken({ | ||||
|         tokenName: 'default-valid', | ||||
|         type: ApiTokenType.CLIENT, | ||||
|         expiresAt: tomorrow, | ||||
|         project: '*', | ||||
|         environment: DEFAULT_ENV, | ||||
|     }); | ||||
| 
 | ||||
|     const tokens = await apiTokenService.getAllActiveTokens(); | ||||
| 
 | ||||
|     expect(tokens.length).toBe(1); | ||||
|     expect(activeToken.secret).toBe(tokens[0].secret); | ||||
| }); | ||||
| 
 | ||||
| test('should create client token with project list', async () => { | ||||
|     const token = await apiTokenService.createApiToken({ | ||||
|         tokenName: 'default-client', | ||||
|  | ||||
							
								
								
									
										95
									
								
								src/test/e2e/services/edge-service.e2e.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/test/e2e/services/edge-service.e2e.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,95 @@ | ||||
| import dbInit, { type ITestDb } from '../helpers/database-init'; | ||||
| import getLogger from '../../fixtures/no-logger'; | ||||
| import { ApiTokenService } from '../../../lib/services/api-token-service'; | ||||
| import { createTestConfig } from '../../config/test-config'; | ||||
| import { | ||||
|     ApiTokenType, | ||||
|     type IApiToken, | ||||
| } from '../../../lib/types/models/api-token'; | ||||
| import { DEFAULT_ENV } from '../../../lib/util/constants'; | ||||
| import { addDays, subDays } from 'date-fns'; | ||||
| import type ProjectService from '../../../lib/features/project/project-service'; | ||||
| import { createProjectService } from '../../../lib/features'; | ||||
| import { EdgeService, EventService } from '../../../lib/services'; | ||||
| import { type IUnleashStores, TEST_AUDIT_USER } from '../../../lib/types'; | ||||
| 
 | ||||
| let db: ITestDb; | ||||
| let stores: IUnleashStores; | ||||
| let edgeService: EdgeService; | ||||
| let projectService: ProjectService; | ||||
| 
 | ||||
| beforeAll(async () => { | ||||
|     const config = createTestConfig({ | ||||
|         server: { baseUriPath: '/test' }, | ||||
|         experimental: { | ||||
|             flags: { | ||||
|                 useMemoizedActiveTokens: true, | ||||
|             }, | ||||
|         }, | ||||
|     }); | ||||
|     db = await dbInit('api_token_service_serial', getLogger); | ||||
|     stores = db.stores; | ||||
|     const eventService = new EventService(stores, config); | ||||
|     const project = { | ||||
|         id: 'test-project', | ||||
|         name: 'Test Project', | ||||
|         description: 'Fancy', | ||||
|         mode: 'open' as const, | ||||
|         defaultStickiness: 'clientId', | ||||
|     }; | ||||
|     const user = await stores.userStore.insert({ | ||||
|         name: 'Some Name', | ||||
|         email: 'test@getunleash.io', | ||||
|     }); | ||||
|     projectService = createProjectService(db.rawDatabase, config); | ||||
| 
 | ||||
|     await projectService.createProject(project, user, TEST_AUDIT_USER); | ||||
| 
 | ||||
|     const apiTokenService = new ApiTokenService(stores, config, eventService); | ||||
|     edgeService = new EdgeService({ apiTokenService }, config); | ||||
| }); | ||||
| 
 | ||||
| afterAll(async () => { | ||||
|     if (db) { | ||||
|         await db.destroy(); | ||||
|     } | ||||
| }); | ||||
| afterEach(async () => { | ||||
|     const tokens = await stores.apiTokenStore.getAll(); | ||||
|     const deleteAll = tokens.map((t: IApiToken) => | ||||
|         stores.apiTokenStore.delete(t.secret), | ||||
|     ); | ||||
|     await Promise.all(deleteAll); | ||||
| }); | ||||
| 
 | ||||
| test('should only return valid tokens', async () => { | ||||
|     const now = Date.now(); | ||||
|     const yesterday = subDays(now, 1); | ||||
|     const tomorrow = addDays(now, 1); | ||||
| 
 | ||||
|     const expiredToken = await stores.apiTokenStore.insert({ | ||||
|         tokenName: 'expired', | ||||
|         secret: 'expired-secret', | ||||
|         type: ApiTokenType.CLIENT, | ||||
|         expiresAt: yesterday, | ||||
|         projects: ['*'], | ||||
|         environment: DEFAULT_ENV, | ||||
|     }); | ||||
| 
 | ||||
|     const activeToken = await stores.apiTokenStore.insert({ | ||||
|         tokenName: 'default-valid', | ||||
|         secret: 'valid-secret', | ||||
|         type: ApiTokenType.CLIENT, | ||||
|         expiresAt: tomorrow, | ||||
|         projects: ['*'], | ||||
|         environment: DEFAULT_ENV, | ||||
|     }); | ||||
| 
 | ||||
|     const response = await edgeService.getValidTokens([ | ||||
|         activeToken.secret, | ||||
|         expiredToken.secret, | ||||
|     ]); | ||||
| 
 | ||||
|     expect(response.tokens.length).toBe(1); | ||||
|     expect(activeToken.secret).toBe(response.tokens[0].token); | ||||
| }); | ||||
							
								
								
									
										2
									
								
								src/test/fixtures/fake-api-token-store.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/test/fixtures/fake-api-token-store.ts
									
									
									
									
										vendored
									
									
								
							| @ -48,7 +48,7 @@ export default class FakeApiTokenStore | ||||
| 
 | ||||
|     async getAllActive(): Promise<IApiToken[]> { | ||||
|         return this.tokens.filter( | ||||
|             (token) => token.expiresAt === null || token.expiresAt > new Date(), | ||||
|             (token) => !token.expiresAt || token.expiresAt > new Date(), | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user