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:
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