mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: optimize private projects for enterprise (#4812)
This commit is contained in:
		
							parent
							
								
									fc8ddbd6ff
								
							
						
					
					
						commit
						ac018447f9
					
				| @ -162,6 +162,7 @@ exports[`should create default config 1`] = ` | ||||
|     "keepExisting": false, | ||||
|   }, | ||||
|   "inlineSegmentConstraints": true, | ||||
|   "isEnterprise": false, | ||||
|   "listen": { | ||||
|     "host": undefined, | ||||
|     "port": 4242, | ||||
|  | ||||
| @ -471,3 +471,19 @@ test.each(['demo', '/demo', '/demo/'])( | ||||
|         expect(config.server.baseUriPath).toBe('/demo'); | ||||
|     }, | ||||
| ); | ||||
| 
 | ||||
| test('Config with enterpriseVersion set and pro environment should set isEnterprise to false', async () => { | ||||
|     let config = createConfig({ | ||||
|         enterpriseVersion: '5.3.0', | ||||
|         ui: { environment: 'pro' }, | ||||
|     }); | ||||
|     expect(config.isEnterprise).toBe(false); | ||||
| }); | ||||
| 
 | ||||
| test('Config with enterpriseVersion set and not pro environment should set isEnterprise to true', async () => { | ||||
|     let config = createConfig({ | ||||
|         enterpriseVersion: '5.3.0', | ||||
|         ui: { environment: 'Enterprise' }, | ||||
|     }); | ||||
|     expect(config.isEnterprise).toBe(true); | ||||
| }); | ||||
|  | ||||
| @ -481,6 +481,11 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { | ||||
|     const clientFeatureCaching = loadClientCachingOptions(options); | ||||
| 
 | ||||
|     const prometheusApi = options.prometheusApi || process.env.PROMETHEUS_API; | ||||
| 
 | ||||
|     const isEnterprise = | ||||
|         Boolean(options.enterpriseVersion) && | ||||
|         ui.environment?.toLowerCase() !== 'pro'; | ||||
| 
 | ||||
|     return { | ||||
|         db, | ||||
|         session, | ||||
| @ -513,6 +518,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { | ||||
|         prometheusApi, | ||||
|         publicFolder: options.publicFolder, | ||||
|         disableScheduler: options.disableScheduler, | ||||
|         isEnterprise: isEnterprise, | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -102,17 +102,19 @@ export class PlaygroundService { | ||||
| 
 | ||||
|         let filteredProjects: typeof projects; | ||||
|         if (this.flagResolver.isEnabled('privateProjects')) { | ||||
|             const accessibleProjects = | ||||
|             const projectAccess = | ||||
|                 await this.privateProjectChecker.getUserAccessibleProjects( | ||||
|                     userId, | ||||
|                 ); | ||||
|             filteredProjects = | ||||
|                 projects === ALL | ||||
|                     ? accessibleProjects | ||||
|                     : projects.filter((project) => | ||||
|                           accessibleProjects.includes(project), | ||||
|                       ); | ||||
|             console.log(accessibleProjects); | ||||
|             if (projectAccess.mode === 'all') { | ||||
|                 filteredProjects = projects; | ||||
|             } else if (projects === ALL) { | ||||
|                 filteredProjects = projectAccess.projects; | ||||
|             } else { | ||||
|                 filteredProjects = projects.filter((project) => | ||||
|                     projectAccess.projects.includes(project), | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const environmentFeatures = await Promise.all( | ||||
|  | ||||
| @ -10,9 +10,12 @@ export const createPrivateProjectChecker = ( | ||||
|     const { getLogger } = config; | ||||
|     const privateProjectStore = new PrivateProjectStore(db, getLogger); | ||||
| 
 | ||||
|     return new PrivateProjectChecker({ | ||||
|         privateProjectStore: privateProjectStore, | ||||
|     }); | ||||
|     return new PrivateProjectChecker( | ||||
|         { | ||||
|             privateProjectStore: privateProjectStore, | ||||
|         }, | ||||
|         config, | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export const createFakePrivateProjectChecker = | ||||
|  | ||||
| @ -1,9 +1,10 @@ | ||||
| import { IPrivateProjectChecker } from './privateProjectCheckerType'; | ||||
| import { Promise } from 'ts-toolbelt/out/Any/Promise'; | ||||
| import { ProjectAccess } from './privateProjectStore'; | ||||
| 
 | ||||
| export class FakePrivateProjectChecker implements IPrivateProjectChecker { | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     async getUserAccessibleProjects(userId: number): Promise<string[]> { | ||||
|     async getUserAccessibleProjects(userId: number): Promise<ProjectAccess> { | ||||
|         throw new Error('Method not implemented.'); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,26 +1,35 @@ | ||||
| import { IUnleashStores } from '../../types'; | ||||
| import { IUnleashConfig, IUnleashStores } from '../../types'; | ||||
| import { IPrivateProjectStore } from './privateProjectStoreType'; | ||||
| import { IPrivateProjectChecker } from './privateProjectCheckerType'; | ||||
| import { ALL_PROJECT_ACCESS, ProjectAccess } from './privateProjectStore'; | ||||
| 
 | ||||
| export class PrivateProjectChecker implements IPrivateProjectChecker { | ||||
|     private privateProjectStore: IPrivateProjectStore; | ||||
| 
 | ||||
|     constructor({ | ||||
|         privateProjectStore, | ||||
|     }: Pick<IUnleashStores, 'privateProjectStore'>) { | ||||
|     private isEnterprise: boolean; | ||||
| 
 | ||||
|     constructor( | ||||
|         { privateProjectStore }: Pick<IUnleashStores, 'privateProjectStore'>, | ||||
|         { isEnterprise }: Pick<IUnleashConfig, 'isEnterprise'>, | ||||
|     ) { | ||||
|         this.privateProjectStore = privateProjectStore; | ||||
|         this.isEnterprise = isEnterprise; | ||||
|     } | ||||
| 
 | ||||
|     async getUserAccessibleProjects(userId: number): Promise<string[]> { | ||||
|         return this.privateProjectStore.getUserAccessibleProjects(userId); | ||||
|     async getUserAccessibleProjects(userId: number): Promise<ProjectAccess> { | ||||
|         return this.isEnterprise | ||||
|             ? this.privateProjectStore.getUserAccessibleProjects(userId) | ||||
|             : Promise.resolve(ALL_PROJECT_ACCESS); | ||||
|     } | ||||
| 
 | ||||
|     async hasAccessToProject( | ||||
|         userId: number, | ||||
|         projectId: string, | ||||
|     ): Promise<boolean> { | ||||
|         return (await this.getUserAccessibleProjects(userId)).includes( | ||||
|             projectId, | ||||
|         const projectAccess = await this.getUserAccessibleProjects(userId); | ||||
|         return ( | ||||
|             projectAccess.mode === 'all' || | ||||
|             projectAccess.projects.includes(projectId) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,6 @@ | ||||
| import { ProjectAccess } from './privateProjectStore'; | ||||
| 
 | ||||
| export interface IPrivateProjectChecker { | ||||
|     getUserAccessibleProjects(userId: number): Promise<string[]>; | ||||
|     getUserAccessibleProjects(userId: number): Promise<ProjectAccess>; | ||||
|     hasAccessToProject(userId: number, projectId: string): Promise<boolean>; | ||||
| } | ||||
|  | ||||
| @ -1,8 +1,20 @@ | ||||
| import { Db } from '../../db/db'; | ||||
| import { Logger, LogProvider } from '../../logger'; | ||||
| import { IPrivateProjectStore } from './privateProjectStoreType'; | ||||
| import { ADMIN_TOKEN_ID } from '../../types'; | ||||
| 
 | ||||
| const ADMIN_TOKEN_ID = -1; | ||||
| export type ProjectAccess = | ||||
|     | { | ||||
|           mode: 'all'; | ||||
|       } | ||||
|     | { | ||||
|           mode: 'limited'; | ||||
|           projects: string[]; | ||||
|       }; | ||||
| 
 | ||||
| export const ALL_PROJECT_ACCESS: ProjectAccess = { | ||||
|     mode: 'all', | ||||
| }; | ||||
| 
 | ||||
| class PrivateProjectStore implements IPrivateProjectStore { | ||||
|     private db: Db; | ||||
| @ -16,10 +28,9 @@ class PrivateProjectStore implements IPrivateProjectStore { | ||||
| 
 | ||||
|     destroy(): void {} | ||||
| 
 | ||||
|     async getUserAccessibleProjects(userId: number): Promise<string[]> { | ||||
|     async getUserAccessibleProjects(userId: number): Promise<ProjectAccess> { | ||||
|         if (userId === ADMIN_TOKEN_ID) { | ||||
|             const allProjects = await this.db('projects').pluck('id'); | ||||
|             return allProjects; | ||||
|             return ALL_PROJECT_ACCESS; | ||||
|         } | ||||
|         const isViewer = await this.db('role_user') | ||||
|             .join('roles', 'role_user.role_id', 'roles.id') | ||||
| @ -32,11 +43,10 @@ class PrivateProjectStore implements IPrivateProjectStore { | ||||
|             .first(); | ||||
| 
 | ||||
|         if (!isViewer || isViewer.count == 0) { | ||||
|             const allProjects = await this.db('projects').pluck('id'); | ||||
|             return allProjects; | ||||
|             return ALL_PROJECT_ACCESS; | ||||
|         } | ||||
| 
 | ||||
|         const accessibleProjects = await this.db | ||||
|         const accessibleProjects: string[] = await this.db | ||||
|             .from((db) => { | ||||
|                 db.distinct() | ||||
|                     .select('projects.id as project_id') | ||||
| @ -46,7 +56,15 @@ class PrivateProjectStore implements IPrivateProjectStore { | ||||
|                         'projects.id', | ||||
|                         'project_settings.project', | ||||
|                     ) | ||||
|                     .where('project_settings.project_mode', '!=', 'private') | ||||
|                     .where((builder) => { | ||||
|                         builder | ||||
|                             .whereNull('project_settings.project') | ||||
|                             .orWhere( | ||||
|                                 'project_settings.project_mode', | ||||
|                                 '!=', | ||||
|                                 'private', | ||||
|                             ); | ||||
|                     }) | ||||
|                     .unionAll((queryBuilder) => { | ||||
|                         queryBuilder | ||||
|                             .select('projects.id as project_id') | ||||
| @ -89,7 +107,7 @@ class PrivateProjectStore implements IPrivateProjectStore { | ||||
|             .select('*') | ||||
|             .pluck('project_id'); | ||||
| 
 | ||||
|         return accessibleProjects; | ||||
|         return { mode: 'limited', projects: accessibleProjects }; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| import { ProjectAccess } from './privateProjectStore'; | ||||
| 
 | ||||
| export interface IPrivateProjectStore { | ||||
|     getUserAccessibleProjects(userId: number): Promise<string[]>; | ||||
|     getUserAccessibleProjects(userId: number): Promise<ProjectAccess>; | ||||
| } | ||||
|  | ||||
| @ -184,16 +184,22 @@ export default class ClientInstanceService { | ||||
|                 await this.privateProjectChecker.getUserAccessibleProjects( | ||||
|                     userId, | ||||
|                 ); | ||||
|             return applications.map((application) => { | ||||
|                 return { | ||||
|                     ...application, | ||||
|                     usage: application.usage?.filter( | ||||
|                         (usageItem) => | ||||
|                             usageItem.project === ALL_PROJECTS || | ||||
|                             accessibleProjects.includes(usageItem.project), | ||||
|                     ), | ||||
|                 }; | ||||
|             }); | ||||
|             if (accessibleProjects.mode === 'all') { | ||||
|                 return applications; | ||||
|             } else { | ||||
|                 return applications.map((application) => { | ||||
|                     return { | ||||
|                         ...application, | ||||
|                         usage: application.usage?.filter( | ||||
|                             (usageItem) => | ||||
|                                 usageItem.project === ALL_PROJECTS || | ||||
|                                 accessibleProjects.projects.includes( | ||||
|                                     usageItem.project, | ||||
|                                 ), | ||||
|                         ), | ||||
|                     }; | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|         return applications; | ||||
|     } | ||||
|  | ||||
| @ -1031,11 +1031,15 @@ class FeatureToggleService { | ||||
|         }); | ||||
| 
 | ||||
|         if (this.flagResolver.isEnabled('privateProjects') && userId) { | ||||
|             const projects = | ||||
|             const projectAccess = | ||||
|                 await this.privateProjectChecker.getUserAccessibleProjects( | ||||
|                     userId, | ||||
|                 ); | ||||
|             return features.filter((f) => projects.includes(f.project)); | ||||
|             return projectAccess.mode === 'all' | ||||
|                 ? features | ||||
|                 : features.filter((f) => | ||||
|                       projectAccess.projects.includes(f.project), | ||||
|                   ); | ||||
|         } | ||||
|         return features; | ||||
|     } | ||||
| @ -1860,11 +1864,17 @@ class FeatureToggleService { | ||||
|     ): Promise<FeatureToggle[]> { | ||||
|         const features = await this.featureToggleStore.getAll({ archived }); | ||||
|         if (this.flagResolver.isEnabled('privateProjects')) { | ||||
|             const projects = | ||||
|             const projectAccess = | ||||
|                 await this.privateProjectChecker.getUserAccessibleProjects( | ||||
|                     userId, | ||||
|                 ); | ||||
|             return features.filter((f) => projects.includes(f.project)); | ||||
|             if (projectAccess.mode === 'all') { | ||||
|                 return features; | ||||
|             } else { | ||||
|                 return features.filter((f) => | ||||
|                     projectAccess.projects.includes(f.project), | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|         return features; | ||||
|     } | ||||
|  | ||||
| @ -201,6 +201,7 @@ export const createServices = ( | ||||
|         changeRequestAccessReadModel, | ||||
|         config, | ||||
|     ); | ||||
| 
 | ||||
|     const privateProjectChecker = db | ||||
|         ? createPrivateProjectChecker(db, config) | ||||
|         : createFakePrivateProjectChecker(); | ||||
|  | ||||
| @ -164,13 +164,18 @@ export default class ProjectService { | ||||
|     ): Promise<IProjectWithCount[]> { | ||||
|         const projects = await this.store.getProjectsWithCounts(query, userId); | ||||
|         if (this.flagResolver.isEnabled('privateProjects') && userId) { | ||||
|             const accessibleProjects = | ||||
|             const projectAccess = | ||||
|                 await this.privateProjectChecker.getUserAccessibleProjects( | ||||
|                     userId, | ||||
|                 ); | ||||
|             return projects.filter((project) => | ||||
|                 accessibleProjects.includes(project.id), | ||||
|             ); | ||||
| 
 | ||||
|             if (projectAccess.mode === 'all') { | ||||
|                 return projects; | ||||
|             } else { | ||||
|                 return projects.filter((project) => | ||||
|                     projectAccess.projects.includes(project.id), | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|         return projects; | ||||
|     } | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { ADMIN } from './permissions'; | ||||
| 
 | ||||
| export const ADMIN_TOKEN_ID = -1; | ||||
| export default class NoAuthUser { | ||||
|     isAPI: boolean; | ||||
| 
 | ||||
| @ -11,7 +12,7 @@ export default class NoAuthUser { | ||||
| 
 | ||||
|     constructor( | ||||
|         username: string = 'unknown', | ||||
|         id: number = -1, | ||||
|         id: number = ADMIN_TOKEN_ID, | ||||
|         permissions: string[] = [ADMIN], | ||||
|     ) { | ||||
|         this.isAPI = true; | ||||
|  | ||||
| @ -215,4 +215,5 @@ export interface IUnleashConfig { | ||||
|     prometheusApi?: string; | ||||
|     publicFolder?: string; | ||||
|     disableScheduler?: boolean; | ||||
|     isEnterprise: boolean; | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user