1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00

feat: optimize private projects for enterprise (#4812)

This commit is contained in:
Jaanus Sellin 2023-09-22 11:54:33 +03:00 committed by GitHub
parent fc8ddbd6ff
commit ac018447f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 134 additions and 50 deletions

View File

@ -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,

View File

@ -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);
});

View File

@ -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,
}; };
} }

View File

@ -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(

View File

@ -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 =

View File

@ -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.');
} }

View File

@ -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)
); );
} }
} }

View File

@ -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>;
} }

View File

@ -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 };
} }
} }

View File

@ -1,3 +1,5 @@
import { ProjectAccess } from './privateProjectStore';
export interface IPrivateProjectStore { export interface IPrivateProjectStore {
getUserAccessibleProjects(userId: number): Promise<string[]>; getUserAccessibleProjects(userId: number): Promise<ProjectAccess>;
} }

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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();

View File

@ -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;
} }

View File

@ -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;

View File

@ -215,4 +215,5 @@ export interface IUnleashConfig {
prometheusApi?: string; prometheusApi?: string;
publicFolder?: string; publicFolder?: string;
disableScheduler?: boolean; disableScheduler?: boolean;
isEnterprise: boolean;
} }