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