mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	API tokens scoped to deleted projects shouldn't give wildcard access (#7499)
If you have SDK tokens scoped to projects that are deleted, you should not get access to any flags with those. --------- Co-authored-by: David Leek <david@getunleash.io>
This commit is contained in:
		
							parent
							
								
									e7d07486a1
								
							
						
					
					
						commit
						225d8a91f1
					
				| @ -76,6 +76,7 @@ exports[`should create default config 1`] = ` | |||||||
|   "flagResolver": FlagResolver { |   "flagResolver": FlagResolver { | ||||||
|     "experiments": { |     "experiments": { | ||||||
|       "adminTokenKillSwitch": false, |       "adminTokenKillSwitch": false, | ||||||
|  |       "allowOrphanedWildcardTokens": false, | ||||||
|       "anonymiseEventLog": false, |       "anonymiseEventLog": false, | ||||||
|       "anonymizeProjectOwners": false, |       "anonymizeProjectOwners": false, | ||||||
|       "automatedActions": false, |       "automatedActions": false, | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ import type { Logger, LogProvider } from '../logger'; | |||||||
| import NotFoundError from '../error/notfound-error'; | import NotFoundError from '../error/notfound-error'; | ||||||
| import type { IApiTokenStore } from '../types/stores/api-token-store'; | import type { IApiTokenStore } from '../types/stores/api-token-store'; | ||||||
| import { | import { | ||||||
|     type ApiTokenType, |     ApiTokenType, | ||||||
|     type IApiToken, |     type IApiToken, | ||||||
|     type IApiTokenCreate, |     type IApiTokenCreate, | ||||||
|     isAllProjects, |     isAllProjects, | ||||||
| @ -13,6 +13,7 @@ import { | |||||||
| import { ALL_PROJECTS } from '../util/constants'; | import { ALL_PROJECTS } from '../util/constants'; | ||||||
| import type { Db } from './db'; | import type { Db } from './db'; | ||||||
| import { inTransaction } from './transaction'; | import { inTransaction } from './transaction'; | ||||||
|  | import type { IFlagResolver } from '../types'; | ||||||
| 
 | 
 | ||||||
| const TABLE = 'api_tokens'; | const TABLE = 'api_tokens'; | ||||||
| const API_LINK_TABLE = 'api_token_project'; | const API_LINK_TABLE = 'api_token_project'; | ||||||
| @ -35,33 +36,44 @@ interface ITokenRow extends ITokenInsert { | |||||||
|     project: string; |     project: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const tokenRowReducer = (acc, tokenRow) => { | const createTokenRowReducer = | ||||||
|     const { project, ...token } = tokenRow; |     (allowOrphanedWildcardTokens: boolean) => (acc, tokenRow) => { | ||||||
|     if (!acc[tokenRow.secret]) { |         const { project, ...token } = tokenRow; | ||||||
|         acc[tokenRow.secret] = { |         if (!acc[tokenRow.secret]) { | ||||||
|             secret: token.secret, |             if ( | ||||||
|             tokenName: token.token_name ? token.token_name : token.username, |                 !allowOrphanedWildcardTokens && | ||||||
|             type: token.type.toLowerCase(), |                 !tokenRow.project && | ||||||
|             project: ALL, |                 !tokenRow.secret.startsWith('*:') && | ||||||
|             projects: [ALL], |                 (tokenRow.type === ApiTokenType.CLIENT || | ||||||
|             environment: token.environment ? token.environment : ALL, |                     tokenRow.type === ApiTokenType.FRONTEND) | ||||||
|             expiresAt: token.expires_at, |             ) { | ||||||
|             createdAt: token.created_at, |                 return acc; | ||||||
|             alias: token.alias, |             } | ||||||
|             seenAt: token.seen_at, | 
 | ||||||
|             username: token.token_name ? token.token_name : token.username, |             acc[tokenRow.secret] = { | ||||||
|         }; |                 secret: token.secret, | ||||||
|     } |                 tokenName: token.token_name ? token.token_name : token.username, | ||||||
|     const currentToken = acc[tokenRow.secret]; |                 type: token.type.toLowerCase(), | ||||||
|     if (tokenRow.project) { |                 project: ALL, | ||||||
|         if (isAllProjects(currentToken.projects)) { |                 projects: [ALL], | ||||||
|             currentToken.projects = []; |                 environment: token.environment ? token.environment : ALL, | ||||||
|  |                 expiresAt: token.expires_at, | ||||||
|  |                 createdAt: token.created_at, | ||||||
|  |                 alias: token.alias, | ||||||
|  |                 seenAt: token.seen_at, | ||||||
|  |                 username: token.token_name ? token.token_name : token.username, | ||||||
|  |             }; | ||||||
|         } |         } | ||||||
|         currentToken.projects.push(tokenRow.project); |         const currentToken = acc[tokenRow.secret]; | ||||||
|         currentToken.project = currentToken.projects.join(','); |         if (tokenRow.project) { | ||||||
|     } |             if (isAllProjects(currentToken.projects)) { | ||||||
|     return acc; |                 currentToken.projects = []; | ||||||
| }; |             } | ||||||
|  |             currentToken.projects.push(tokenRow.project); | ||||||
|  |             currentToken.project = currentToken.projects.join(','); | ||||||
|  |         } | ||||||
|  |         return acc; | ||||||
|  |     }; | ||||||
| 
 | 
 | ||||||
| const toRow = (newToken: IApiTokenCreate) => ({ | const toRow = (newToken: IApiTokenCreate) => ({ | ||||||
|     username: newToken.tokenName ?? newToken.username, |     username: newToken.tokenName ?? newToken.username, | ||||||
| @ -74,8 +86,14 @@ const toRow = (newToken: IApiTokenCreate) => ({ | |||||||
|     alias: newToken.alias || null, |     alias: newToken.alias || null, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const toTokens = (rows: any[]): IApiToken[] => { | const toTokens = ( | ||||||
|     const tokens = rows.reduce(tokenRowReducer, {}); |     rows: any[], | ||||||
|  |     allowOrphanedWildcardTokens: boolean, | ||||||
|  | ): IApiToken[] => { | ||||||
|  |     const tokens = rows.reduce( | ||||||
|  |         createTokenRowReducer(allowOrphanedWildcardTokens), | ||||||
|  |         {}, | ||||||
|  |     ); | ||||||
|     return Object.values(tokens); |     return Object.values(tokens); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| @ -86,7 +104,14 @@ export class ApiTokenStore implements IApiTokenStore { | |||||||
| 
 | 
 | ||||||
|     private db: Db; |     private db: Db; | ||||||
| 
 | 
 | ||||||
|     constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) { |     private readonly flagResolver: IFlagResolver; | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         db: Db, | ||||||
|  |         eventBus: EventEmitter, | ||||||
|  |         getLogger: LogProvider, | ||||||
|  |         flagResolver: IFlagResolver, | ||||||
|  |     ) { | ||||||
|         this.db = db; |         this.db = db; | ||||||
|         this.logger = getLogger('api-tokens.js'); |         this.logger = getLogger('api-tokens.js'); | ||||||
|         this.timer = (action: string) => |         this.timer = (action: string) => | ||||||
| @ -94,6 +119,7 @@ export class ApiTokenStore implements IApiTokenStore { | |||||||
|                 store: 'api-tokens', |                 store: 'api-tokens', | ||||||
|                 action, |                 action, | ||||||
|             }); |             }); | ||||||
|  |         this.flagResolver = flagResolver; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async count(): Promise<number> { |     async count(): Promise<number> { | ||||||
| @ -120,7 +146,10 @@ export class ApiTokenStore implements IApiTokenStore { | |||||||
|         const stopTimer = this.timer('getAll'); |         const stopTimer = this.timer('getAll'); | ||||||
|         const rows = await this.makeTokenProjectQuery(); |         const rows = await this.makeTokenProjectQuery(); | ||||||
|         stopTimer(); |         stopTimer(); | ||||||
|         return toTokens(rows); |         const allowOrphanedWildcardTokens = this.flagResolver.isEnabled( | ||||||
|  |             'allowOrphanedWildcardTokens', | ||||||
|  |         ); | ||||||
|  |         return toTokens(rows, allowOrphanedWildcardTokens); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getAllActive(): Promise<IApiToken[]> { |     async getAllActive(): Promise<IApiToken[]> { | ||||||
| @ -129,7 +158,10 @@ export class ApiTokenStore implements IApiTokenStore { | |||||||
|             .where('expires_at', 'IS', null) |             .where('expires_at', 'IS', null) | ||||||
|             .orWhere('expires_at', '>', 'now()'); |             .orWhere('expires_at', '>', 'now()'); | ||||||
|         stopTimer(); |         stopTimer(); | ||||||
|         return toTokens(rows); |         const allowOrphanedWildcardTokens = this.flagResolver.isEnabled( | ||||||
|  |             'allowOrphanedWildcardTokens', | ||||||
|  |         ); | ||||||
|  |         return toTokens(rows, allowOrphanedWildcardTokens); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private makeTokenProjectQuery() { |     private makeTokenProjectQuery() { | ||||||
| @ -200,7 +232,10 @@ export class ApiTokenStore implements IApiTokenStore { | |||||||
|             key, |             key, | ||||||
|         ); |         ); | ||||||
|         stopTimer(); |         stopTimer(); | ||||||
|         return toTokens(row)[0]; |         const allowOrphanedWildcardTokens = this.flagResolver.isEnabled( | ||||||
|  |             'allowOrphanedWildcardTokens', | ||||||
|  |         ); | ||||||
|  |         return toTokens(row, allowOrphanedWildcardTokens)[0]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async delete(secret: string): Promise<void> { |     async delete(secret: string): Promise<void> { | ||||||
| @ -217,7 +252,10 @@ export class ApiTokenStore implements IApiTokenStore { | |||||||
|             .where({ secret }) |             .where({ secret }) | ||||||
|             .returning('*'); |             .returning('*'); | ||||||
|         if (rows.length > 0) { |         if (rows.length > 0) { | ||||||
|             return toTokens(rows)[0]; |             const allowOrphanedWildcardTokens = this.flagResolver.isEnabled( | ||||||
|  |                 'allowOrphanedWildcardTokens', | ||||||
|  |             ); | ||||||
|  |             return toTokens(rows, allowOrphanedWildcardTokens)[0]; | ||||||
|         } |         } | ||||||
|         throw new NotFoundError('Could not find api-token.'); |         throw new NotFoundError('Could not find api-token.'); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -97,7 +97,12 @@ export const createStores = ( | |||||||
|         tagTypeStore: new TagTypeStore(db, eventBus, getLogger), |         tagTypeStore: new TagTypeStore(db, eventBus, getLogger), | ||||||
|         addonStore: new AddonStore(db, eventBus, getLogger), |         addonStore: new AddonStore(db, eventBus, getLogger), | ||||||
|         accessStore: new AccessStore(db, eventBus, getLogger), |         accessStore: new AccessStore(db, eventBus, getLogger), | ||||||
|         apiTokenStore: new ApiTokenStore(db, eventBus, getLogger), |         apiTokenStore: new ApiTokenStore( | ||||||
|  |             db, | ||||||
|  |             eventBus, | ||||||
|  |             getLogger, | ||||||
|  |             config.flagResolver, | ||||||
|  |         ), | ||||||
|         resetTokenStore: new ResetTokenStore(db, eventBus, getLogger), |         resetTokenStore: new ResetTokenStore(db, eventBus, getLogger), | ||||||
|         sessionStore: new SessionStore(db, eventBus, getLogger), |         sessionStore: new SessionStore(db, eventBus, getLogger), | ||||||
|         userFeedbackStore: new UserFeedbackStore(db, eventBus, getLogger), |         userFeedbackStore: new UserFeedbackStore(db, eventBus, getLogger), | ||||||
|  | |||||||
| @ -15,7 +15,12 @@ export const createApiTokenService = ( | |||||||
|     config: IUnleashConfig, |     config: IUnleashConfig, | ||||||
| ): ApiTokenService => { | ): ApiTokenService => { | ||||||
|     const { eventBus, getLogger } = config; |     const { eventBus, getLogger } = config; | ||||||
|     const apiTokenStore = new ApiTokenStore(db, eventBus, getLogger); |     const apiTokenStore = new ApiTokenStore( | ||||||
|  |         db, | ||||||
|  |         eventBus, | ||||||
|  |         getLogger, | ||||||
|  |         config.flagResolver, | ||||||
|  |     ); | ||||||
|     const environmentStore = new EnvironmentStore(db, eventBus, getLogger); |     const environmentStore = new EnvironmentStore(db, eventBus, getLogger); | ||||||
|     const eventService = createEventsService(db, config); |     const eventService = createEventsService(db, config); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -78,7 +78,12 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => { | |||||||
|         getLogger, |         getLogger, | ||||||
|     ); |     ); | ||||||
|     const eventStore = new EventStore(db, getLogger); |     const eventStore = new EventStore(db, getLogger); | ||||||
|     const apiTokenStore = new ApiTokenStore(db, eventBus, getLogger); |     const apiTokenStore = new ApiTokenStore( | ||||||
|  |         db, | ||||||
|  |         eventBus, | ||||||
|  |         getLogger, | ||||||
|  |         flagResolver, | ||||||
|  |     ); | ||||||
|     const clientMetricsStoreV2 = new ClientMetricsStoreV2( |     const clientMetricsStoreV2 = new ClientMetricsStoreV2( | ||||||
|         db, |         db, | ||||||
|         getLogger, |         getLogger, | ||||||
|  | |||||||
| @ -63,6 +63,7 @@ export type IFlagKey = | |||||||
|     | 'flagCreator' |     | 'flagCreator' | ||||||
|     | 'anonymizeProjectOwners' |     | 'anonymizeProjectOwners' | ||||||
|     | 'resourceLimits' |     | 'resourceLimits' | ||||||
|  |     | 'allowOrphanedWildcardTokens' | ||||||
|     | 'extendedMetrics'; |     | 'extendedMetrics'; | ||||||
| 
 | 
 | ||||||
| export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; | export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; | ||||||
| @ -300,6 +301,10 @@ const flags: IFlags = { | |||||||
|         process.env.UNLEASH_EXPERIMENTAL_RESOURCE_LIMITS, |         process.env.UNLEASH_EXPERIMENTAL_RESOURCE_LIMITS, | ||||||
|         false, |         false, | ||||||
|     ), |     ), | ||||||
|  |     allowOrphanedWildcardTokens: parseEnvVarBoolean( | ||||||
|  |         process.env.UNLEASH_ORPHANED_TOKENS_KILL_SWITCH, | ||||||
|  |         false, | ||||||
|  |     ), | ||||||
|     extendedMetrics: parseEnvVarBoolean( |     extendedMetrics: parseEnvVarBoolean( | ||||||
|         process.env.UNLEASH_EXPERIMENTAL_EXTENDED_METRICS, |         process.env.UNLEASH_EXPERIMENTAL_EXTENDED_METRICS, | ||||||
|         false, |         false, | ||||||
|  | |||||||
| @ -79,7 +79,7 @@ test('editor users should only get client or frontend tokens', async () => { | |||||||
|         projects: [], |         projects: [], | ||||||
|         tokenName: '', |         tokenName: '', | ||||||
|         username: 'test', |         username: 'test', | ||||||
|         secret: '1234', |         secret: '*:environment.1234', | ||||||
|         type: ApiTokenType.CLIENT, |         type: ApiTokenType.CLIENT, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @ -88,7 +88,7 @@ test('editor users should only get client or frontend tokens', async () => { | |||||||
|         projects: [], |         projects: [], | ||||||
|         tokenName: '', |         tokenName: '', | ||||||
|         username: 'frontend', |         username: 'frontend', | ||||||
|         secret: '12345', |         secret: '*:environment.12345', | ||||||
|         type: ApiTokenType.FRONTEND, |         type: ApiTokenType.FRONTEND, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @ -97,7 +97,7 @@ test('editor users should only get client or frontend tokens', async () => { | |||||||
|         projects: [], |         projects: [], | ||||||
|         tokenName: '', |         tokenName: '', | ||||||
|         username: 'test', |         username: 'test', | ||||||
|         secret: 'sdfsdf2d', |         secret: '*:*.sdfsdf2d', | ||||||
|         type: ApiTokenType.ADMIN, |         type: ApiTokenType.ADMIN, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @ -141,7 +141,7 @@ test('viewer users should not be allowed to fetch tokens', async () => { | |||||||
|         projects: [], |         projects: [], | ||||||
|         tokenName: '', |         tokenName: '', | ||||||
|         username: 'test', |         username: 'test', | ||||||
|         secret: '1234', |         secret: '*:environment.1234', | ||||||
|         type: ApiTokenType.CLIENT, |         type: ApiTokenType.CLIENT, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @ -150,7 +150,7 @@ test('viewer users should not be allowed to fetch tokens', async () => { | |||||||
|         projects: [], |         projects: [], | ||||||
|         tokenName: '', |         tokenName: '', | ||||||
|         username: 'test', |         username: 'test', | ||||||
|         secret: 'sdfsdf2d', |         secret: '*:*.sdfsdf2d', | ||||||
|         type: ApiTokenType.ADMIN, |         type: ApiTokenType.ADMIN, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @ -553,7 +553,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                 tokenName: '', |                 tokenName: '', | ||||||
| 
 | 
 | ||||||
|                 username: 'client', |                 username: 'client', | ||||||
|                 secret: 'client_secret', |                 secret: '*:environment.client_secret', | ||||||
|                 type: ApiTokenType.CLIENT, |                 type: ApiTokenType.CLIENT, | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
| @ -562,7 +562,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                 projects: [], |                 projects: [], | ||||||
|                 tokenName: '', |                 tokenName: '', | ||||||
|                 username: 'admin', |                 username: 'admin', | ||||||
|                 secret: 'sdfsdf2admin_secret', |                 secret: '*:*.sdfsdf2admin_secret', | ||||||
|                 type: ApiTokenType.ADMIN, |                 type: ApiTokenType.ADMIN, | ||||||
|             }); |             }); | ||||||
|             await stores.apiTokenStore.insert({ |             await stores.apiTokenStore.insert({ | ||||||
| @ -570,7 +570,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                 projects: [], |                 projects: [], | ||||||
|                 tokenName: '', |                 tokenName: '', | ||||||
|                 username: 'frontender', |                 username: 'frontender', | ||||||
|                 secret: 'sdfsdf2dfrontend_Secret', |                 secret: '*:environment:sdfsdf2dfrontend_Secret', | ||||||
|                 type: ApiTokenType.FRONTEND, |                 type: ApiTokenType.FRONTEND, | ||||||
|             }); |             }); | ||||||
|             await request |             await request | ||||||
| @ -637,7 +637,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                 projects: [], |                 projects: [], | ||||||
|                 tokenName: '', |                 tokenName: '', | ||||||
|                 username: 'client', |                 username: 'client', | ||||||
|                 secret: 'client_secret_1234', |                 secret: '*:environment.client_secret_1234', | ||||||
|                 type: ApiTokenType.CLIENT, |                 type: ApiTokenType.CLIENT, | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
| @ -646,7 +646,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                 projects: [], |                 projects: [], | ||||||
|                 tokenName: '', |                 tokenName: '', | ||||||
|                 username: 'admin', |                 username: 'admin', | ||||||
|                 secret: 'admin_secret_1234', |                 secret: '*:*.admin_secret_1234', | ||||||
|                 type: ApiTokenType.ADMIN, |                 type: ApiTokenType.ADMIN, | ||||||
|             }); |             }); | ||||||
|             await stores.apiTokenStore.insert({ |             await stores.apiTokenStore.insert({ | ||||||
| @ -654,7 +654,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                 projects: [], |                 projects: [], | ||||||
|                 tokenName: '', |                 tokenName: '', | ||||||
|                 username: 'frontender', |                 username: 'frontender', | ||||||
|                 secret: 'frontend_secret_1234', |                 secret: '*:environment.frontend_secret_1234', | ||||||
|                 type: ApiTokenType.FRONTEND, |                 type: ApiTokenType.FRONTEND, | ||||||
|             }); |             }); | ||||||
|             await request |             await request | ||||||
| @ -699,7 +699,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                 projects: [], |                 projects: [], | ||||||
|                 tokenName: '', |                 tokenName: '', | ||||||
|                 username: 'client', |                 username: 'client', | ||||||
|                 secret: 'client_secret_4321', |                 secret: '*:environment.client_secret_4321', | ||||||
|                 type: ApiTokenType.CLIENT, |                 type: ApiTokenType.CLIENT, | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
| @ -708,7 +708,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                 projects: [], |                 projects: [], | ||||||
|                 tokenName: '', |                 tokenName: '', | ||||||
|                 username: 'admin', |                 username: 'admin', | ||||||
|                 secret: 'admin_secret_4321', |                 secret: '*:*.admin_secret_4321', | ||||||
|                 type: ApiTokenType.ADMIN, |                 type: ApiTokenType.ADMIN, | ||||||
|             }); |             }); | ||||||
|             await stores.apiTokenStore.insert({ |             await stores.apiTokenStore.insert({ | ||||||
| @ -716,7 +716,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                 projects: [], |                 projects: [], | ||||||
|                 tokenName: '', |                 tokenName: '', | ||||||
|                 username: 'frontender', |                 username: 'frontender', | ||||||
|                 secret: 'frontend_secret_4321', |                 secret: '*:environment.frontend_secret_4321', | ||||||
|                 type: ApiTokenType.FRONTEND, |                 type: ApiTokenType.FRONTEND, | ||||||
|             }); |             }); | ||||||
|             await request |             await request | ||||||
| @ -760,7 +760,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                 projects: [], |                 projects: [], | ||||||
|                 tokenName: '', |                 tokenName: '', | ||||||
|                 username: 'client', |                 username: 'client', | ||||||
|                 secret: 'client_secret_4321', |                 secret: '*:environment.client_secret_4321', | ||||||
|                 type: ApiTokenType.CLIENT, |                 type: ApiTokenType.CLIENT, | ||||||
|             }); |             }); | ||||||
|             await stores.apiTokenStore.insert({ |             await stores.apiTokenStore.insert({ | ||||||
| @ -768,7 +768,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                 projects: [], |                 projects: [], | ||||||
|                 tokenName: '', |                 tokenName: '', | ||||||
|                 username: 'admin', |                 username: 'admin', | ||||||
|                 secret: 'admin_secret_4321', |                 secret: '*:*.admin_secret_4321', | ||||||
|                 type: ApiTokenType.ADMIN, |                 type: ApiTokenType.ADMIN, | ||||||
|             }); |             }); | ||||||
|             await stores.apiTokenStore.insert({ |             await stores.apiTokenStore.insert({ | ||||||
| @ -776,7 +776,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                 projects: [], |                 projects: [], | ||||||
|                 tokenName: '', |                 tokenName: '', | ||||||
|                 username: 'frontender', |                 username: 'frontender', | ||||||
|                 secret: 'frontend_secret_4321', |                 secret: '*:environment.frontend_secret_4321', | ||||||
|                 type: ApiTokenType.FRONTEND, |                 type: ApiTokenType.FRONTEND, | ||||||
|             }); |             }); | ||||||
|             await request |             await request | ||||||
| @ -848,7 +848,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                     projects: [], |                     projects: [], | ||||||
|                     tokenName: '', |                     tokenName: '', | ||||||
|                     username: 'cilent', |                     username: 'cilent', | ||||||
|                     secret: 'update_client_token', |                     secret: '*:environment.update_client_token', | ||||||
|                     type: ApiTokenType.CLIENT, |                     type: ApiTokenType.CLIENT, | ||||||
|                 }); |                 }); | ||||||
|                 await request |                 await request | ||||||
| @ -910,7 +910,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                     projects: [], |                     projects: [], | ||||||
|                     tokenName: '', |                     tokenName: '', | ||||||
|                     username: 'frontend', |                     username: 'frontend', | ||||||
|                     secret: 'update_frontend_token', |                     secret: '*:environment.update_frontend_token', | ||||||
|                     type: ApiTokenType.FRONTEND, |                     type: ApiTokenType.FRONTEND, | ||||||
|                 }); |                 }); | ||||||
|                 await request |                 await request | ||||||
| @ -973,7 +973,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                     tokenName: '', |                     tokenName: '', | ||||||
| 
 | 
 | ||||||
|                     username: 'admin', |                     username: 'admin', | ||||||
|                     secret: 'update_admin_token', |                     secret: '*:*.update_admin_token', | ||||||
|                     type: ApiTokenType.ADMIN, |                     type: ApiTokenType.ADMIN, | ||||||
|                 }); |                 }); | ||||||
|                 await request |                 await request | ||||||
| @ -1038,7 +1038,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                     projects: [], |                     projects: [], | ||||||
|                     tokenName: '', |                     tokenName: '', | ||||||
|                     username: 'cilent', |                     username: 'cilent', | ||||||
|                     secret: 'delete_client_token', |                     secret: '*:environment.delete_client_token', | ||||||
|                     type: ApiTokenType.CLIENT, |                     type: ApiTokenType.CLIENT, | ||||||
|                 }); |                 }); | ||||||
|                 await request |                 await request | ||||||
| @ -1100,7 +1100,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                     projects: [], |                     projects: [], | ||||||
|                     tokenName: '', |                     tokenName: '', | ||||||
|                     username: 'frontend', |                     username: 'frontend', | ||||||
|                     secret: 'delete_frontend_token', |                     secret: '*:environment.delete_frontend_token', | ||||||
|                     type: ApiTokenType.FRONTEND, |                     type: ApiTokenType.FRONTEND, | ||||||
|                 }); |                 }); | ||||||
|                 await request |                 await request | ||||||
| @ -1161,7 +1161,7 @@ describe('Fine grained API token permissions', () => { | |||||||
|                     projects: [], |                     projects: [], | ||||||
|                     tokenName: '', |                     tokenName: '', | ||||||
|                     username: 'admin', |                     username: 'admin', | ||||||
|                     secret: 'delete_admin_token', |                     secret: '*:*:delete_admin_token', | ||||||
|                     type: ApiTokenType.ADMIN, |                     type: ApiTokenType.ADMIN, | ||||||
|                 }); |                 }); | ||||||
|                 await request |                 await request | ||||||
|  | |||||||
| @ -122,7 +122,7 @@ test('creates new admin token with expiry', async () => { | |||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('update client token with expiry', async () => { | test('update client token with expiry', async () => { | ||||||
|     const tokenSecret = 'random-secret-update'; |     const tokenSecret = '*:environment.random-secret-update'; | ||||||
| 
 | 
 | ||||||
|     await db.stores.apiTokenStore.insert({ |     await db.stores.apiTokenStore.insert({ | ||||||
|         username: 'test', |         username: 'test', | ||||||
| @ -187,7 +187,7 @@ test('creates a lot of client tokens', async () => { | |||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('removes api token', async () => { | test('removes api token', async () => { | ||||||
|     const tokenSecret = 'random-secret'; |     const tokenSecret = '*:environment.random-secret'; | ||||||
| 
 | 
 | ||||||
|     await db.stores.apiTokenStore.insert({ |     await db.stores.apiTokenStore.insert({ | ||||||
|         environment: 'development', |         environment: 'development', | ||||||
|  | |||||||
| @ -0,0 +1,153 @@ | |||||||
|  | import { type IUnleashTest, setupAppWithAuth } from '../../helpers/test-helper'; | ||||||
|  | import dbInit, { type ITestDb } from '../../helpers/database-init'; | ||||||
|  | import getLogger from '../../../fixtures/no-logger'; | ||||||
|  | import type { ApiTokenService } from '../../../../lib/services/api-token-service'; | ||||||
|  | import { ApiTokenType } from '../../../../lib/types/models/api-token'; | ||||||
|  | import { DEFAULT_ENV } from '../../../../lib/util/constants'; | ||||||
|  | import { TEST_AUDIT_USER } from '../../../../lib/types'; | ||||||
|  | import { User } from '../../../../lib/server-impl'; | ||||||
|  | 
 | ||||||
|  | let app: IUnleashTest; | ||||||
|  | let db: ITestDb; | ||||||
|  | 
 | ||||||
|  | let apiTokenService: ApiTokenService; | ||||||
|  | 
 | ||||||
|  | const environment = 'testing'; | ||||||
|  | const project = 'default'; | ||||||
|  | const project2 = 'some'; | ||||||
|  | const deletionProject = 'deletion'; | ||||||
|  | const deletionTokenName = 'delete'; | ||||||
|  | const feature1 = 'f1.token.access'; | ||||||
|  | const feature2 = 'f2.token.access'; | ||||||
|  | const feature3 = 'f3.p2.token.access'; | ||||||
|  | 
 | ||||||
|  | beforeAll(async () => { | ||||||
|  |     db = await dbInit('feature_api_api_access_client_deletion', getLogger); | ||||||
|  |     app = await setupAppWithAuth(db.stores, {}, db.rawDatabase); | ||||||
|  |     apiTokenService = app.services.apiTokenService; | ||||||
|  | 
 | ||||||
|  |     const { featureToggleServiceV2, environmentService } = app.services; | ||||||
|  |     const { environmentStore, projectStore } = db.stores; | ||||||
|  | 
 | ||||||
|  |     await environmentStore.create({ | ||||||
|  |         name: environment, | ||||||
|  |         type: 'test', | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     await projectStore.create({ | ||||||
|  |         id: project2, | ||||||
|  |         name: 'Test Project 2', | ||||||
|  |         description: '', | ||||||
|  |         mode: 'open' as const, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     await projectStore.create({ | ||||||
|  |         id: deletionProject, | ||||||
|  |         name: 'Deletion Project', | ||||||
|  |         description: '', | ||||||
|  |         mode: 'open' as const, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     await environmentService.addEnvironmentToProject( | ||||||
|  |         environment, | ||||||
|  |         project, | ||||||
|  |         TEST_AUDIT_USER, | ||||||
|  |     ); | ||||||
|  |     await environmentService.addEnvironmentToProject( | ||||||
|  |         environment, | ||||||
|  |         project2, | ||||||
|  |         TEST_AUDIT_USER, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     await featureToggleServiceV2.createFeatureToggle( | ||||||
|  |         project, | ||||||
|  |         { | ||||||
|  |             name: feature1, | ||||||
|  |             description: 'the #1 feature', | ||||||
|  |         }, | ||||||
|  |         TEST_AUDIT_USER, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     await featureToggleServiceV2.createStrategy( | ||||||
|  |         { | ||||||
|  |             name: 'default', | ||||||
|  |             constraints: [], | ||||||
|  |             parameters: {}, | ||||||
|  |         }, | ||||||
|  |         { projectId: project, featureName: feature1, environment: DEFAULT_ENV }, | ||||||
|  |         TEST_AUDIT_USER, | ||||||
|  |     ); | ||||||
|  |     await featureToggleServiceV2.createStrategy( | ||||||
|  |         { | ||||||
|  |             name: 'default', | ||||||
|  |             constraints: [], | ||||||
|  |             parameters: {}, | ||||||
|  |         }, | ||||||
|  |         { projectId: project, featureName: feature1, environment }, | ||||||
|  |         TEST_AUDIT_USER, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     // create feature 2
 | ||||||
|  |     await featureToggleServiceV2.createFeatureToggle( | ||||||
|  |         project, | ||||||
|  |         { | ||||||
|  |             name: feature2, | ||||||
|  |         }, | ||||||
|  |         TEST_AUDIT_USER, | ||||||
|  |     ); | ||||||
|  |     await featureToggleServiceV2.createStrategy( | ||||||
|  |         { | ||||||
|  |             name: 'default', | ||||||
|  |             constraints: [], | ||||||
|  |             parameters: {}, | ||||||
|  |         }, | ||||||
|  |         { projectId: project, featureName: feature2, environment }, | ||||||
|  |         TEST_AUDIT_USER, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     // create feature 3
 | ||||||
|  |     await featureToggleServiceV2.createFeatureToggle( | ||||||
|  |         project2, | ||||||
|  |         { | ||||||
|  |             name: feature3, | ||||||
|  |         }, | ||||||
|  |         TEST_AUDIT_USER, | ||||||
|  |     ); | ||||||
|  |     await featureToggleServiceV2.createStrategy( | ||||||
|  |         { | ||||||
|  |             name: 'default', | ||||||
|  |             constraints: [], | ||||||
|  |             parameters: {}, | ||||||
|  |         }, | ||||||
|  |         { projectId: project2, featureName: feature3, environment }, | ||||||
|  |         TEST_AUDIT_USER, | ||||||
|  |     ); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | afterAll(async () => { | ||||||
|  |     await app.destroy(); | ||||||
|  |     await db.destroy(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('doesnt return feature flags if project deleted', async () => { | ||||||
|  |     const token = await apiTokenService.createApiToken({ | ||||||
|  |         type: ApiTokenType.CLIENT, | ||||||
|  |         tokenName: deletionTokenName, | ||||||
|  |         environment, | ||||||
|  |         project: deletionProject, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     await app.services.projectService.deleteProject( | ||||||
|  |         deletionProject, | ||||||
|  |         new User(TEST_AUDIT_USER), | ||||||
|  |         TEST_AUDIT_USER, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     await app.services.apiTokenService.fetchActiveTokens(); | ||||||
|  | 
 | ||||||
|  |     await app.request | ||||||
|  |         .get('/api/client/features') | ||||||
|  |         .set('Authorization', token.secret) | ||||||
|  |         .expect('Content-Type', /json/) | ||||||
|  |         .expect(401); | ||||||
|  | }); | ||||||
| @ -69,7 +69,7 @@ test('should only return valid tokens', async () => { | |||||||
| 
 | 
 | ||||||
|     const expiredToken = await stores.apiTokenStore.insert({ |     const expiredToken = await stores.apiTokenStore.insert({ | ||||||
|         tokenName: 'expired', |         tokenName: 'expired', | ||||||
|         secret: 'expired-secret', |         secret: '*:environment.expired-secret', | ||||||
|         type: ApiTokenType.CLIENT, |         type: ApiTokenType.CLIENT, | ||||||
|         expiresAt: yesterday, |         expiresAt: yesterday, | ||||||
|         projects: ['*'], |         projects: ['*'], | ||||||
| @ -78,7 +78,7 @@ test('should only return valid tokens', async () => { | |||||||
| 
 | 
 | ||||||
|     const activeToken = await stores.apiTokenStore.insert({ |     const activeToken = await stores.apiTokenStore.insert({ | ||||||
|         tokenName: 'default-valid', |         tokenName: 'default-valid', | ||||||
|         secret: 'valid-secret', |         secret: '*:environment.valid-secret', | ||||||
|         type: ApiTokenType.CLIENT, |         type: ApiTokenType.CLIENT, | ||||||
|         expiresAt: tomorrow, |         expiresAt: tomorrow, | ||||||
|         projects: ['*'], |         projects: ['*'], | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user