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