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,
},
"inlineSegmentConstraints": true,
"isEnterprise": false,
"listen": {
"host": undefined,
"port": 4242,

View File

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

View File

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

View File

@ -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),
if (projectAccess.mode === 'all') {
filteredProjects = projects;
} else if (projects === ALL) {
filteredProjects = projectAccess.projects;
} else {
filteredProjects = projects.filter((project) =>
projectAccess.projects.includes(project),
);
console.log(accessibleProjects);
}
}
const environmentFeatures = await Promise.all(

View File

@ -10,9 +10,12 @@ export const createPrivateProjectChecker = (
const { getLogger } = config;
const privateProjectStore = new PrivateProjectStore(db, getLogger);
return new PrivateProjectChecker({
return new PrivateProjectChecker(
{
privateProjectStore: privateProjectStore,
});
},
config,
);
};
export const createFakePrivateProjectChecker =

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -184,17 +184,23 @@ export default class ClientInstanceService {
await this.privateProjectChecker.getUserAccessibleProjects(
userId,
);
if (accessibleProjects.mode === 'all') {
return applications;
} else {
return applications.map((application) => {
return {
...application,
usage: application.usage?.filter(
(usageItem) =>
usageItem.project === ALL_PROJECTS ||
accessibleProjects.includes(usageItem.project),
accessibleProjects.projects.includes(
usageItem.project,
),
),
};
});
}
}
return applications;
}

View File

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

View File

@ -201,6 +201,7 @@ export const createServices = (
changeRequestAccessReadModel,
config,
);
const privateProjectChecker = db
? createPrivateProjectChecker(db, config)
: createFakePrivateProjectChecker();

View File

@ -164,14 +164,19 @@ 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,
);
if (projectAccess.mode === 'all') {
return projects;
} else {
return projects.filter((project) =>
accessibleProjects.includes(project.id),
projectAccess.projects.includes(project.id),
);
}
}
return projects;
}

View File

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

View File

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