mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
feat: private project filtering and store implementation (#4758)
This commit is contained in:
parent
2928857857
commit
39d2d065cd
@ -42,7 +42,7 @@ import {
|
||||
import StrategyStore from '../../db/strategy-store';
|
||||
import FakeStrategiesStore from '../../../test/fixtures/fake-strategies-store';
|
||||
import {
|
||||
createFakeprivateProjectChecker,
|
||||
createFakePrivateProjectChecker,
|
||||
createPrivateProjectChecker,
|
||||
} from '../private-project/createPrivateProjectChecker';
|
||||
|
||||
@ -155,7 +155,7 @@ export const createFakeFeatureToggleService = (
|
||||
);
|
||||
const segmentService = createFakeSegmentService(config);
|
||||
const changeRequestAccessReadModel = createFakeChangeRequestAccessService();
|
||||
const fakeprivateProjectChecker = createFakeprivateProjectChecker();
|
||||
const fakeprivateProjectChecker = createFakePrivateProjectChecker();
|
||||
const featureToggleService = new FeatureToggleService(
|
||||
{
|
||||
featureStrategiesStore,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Db, IUnleashConfig } from 'lib/server-impl';
|
||||
import PrivateProjectStore from './privateProjectStore';
|
||||
import { PrivateProjectChecker } from './privateProjectChecker';
|
||||
import { FakeprivateProjectChecker } from './fakePrivateProjectChecker';
|
||||
import { FakePrivateProjectChecker } from './fakePrivateProjectChecker';
|
||||
|
||||
export const createPrivateProjectChecker = (
|
||||
db: Db,
|
||||
@ -15,7 +15,7 @@ export const createPrivateProjectChecker = (
|
||||
});
|
||||
};
|
||||
|
||||
export const createFakeprivateProjectChecker =
|
||||
(): FakeprivateProjectChecker => {
|
||||
return new FakeprivateProjectChecker();
|
||||
export const createFakePrivateProjectChecker =
|
||||
(): FakePrivateProjectChecker => {
|
||||
return new FakePrivateProjectChecker();
|
||||
};
|
||||
|
@ -1,8 +1,14 @@
|
||||
import { IPrivateProjectChecker } from './privateProjectCheckerType';
|
||||
import { Promise } from 'ts-toolbelt/out/Any/Promise';
|
||||
|
||||
export class FakeprivateProjectChecker implements IPrivateProjectChecker {
|
||||
export class FakePrivateProjectChecker implements IPrivateProjectChecker {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async getUserAccessibleProjects(userId: number): Promise<string[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
hasAccessToProject(userId: number, projectId: string): Promise<boolean> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
@ -14,4 +14,13 @@ export class PrivateProjectChecker implements IPrivateProjectChecker {
|
||||
async getUserAccessibleProjects(userId: number): Promise<string[]> {
|
||||
return this.privateProjectStore.getUserAccessibleProjects(userId);
|
||||
}
|
||||
|
||||
async hasAccessToProject(
|
||||
userId: number,
|
||||
projectId: string,
|
||||
): Promise<boolean> {
|
||||
return (await this.getUserAccessibleProjects(userId)).includes(
|
||||
projectId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
export interface IPrivateProjectChecker {
|
||||
getUserAccessibleProjects(userId: number): Promise<string[]>;
|
||||
hasAccessToProject(userId: number, projectId: string): Promise<boolean>;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ const privateProjectMiddleware = (
|
||||
getLogger,
|
||||
flagResolver,
|
||||
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
|
||||
{ projectService, accessService }: IUnleashServices,
|
||||
{ accessService, privateProjectChecker }: IUnleashServices,
|
||||
): any => {
|
||||
const logger = getLogger('/middleware/project-middleware.ts');
|
||||
logger.debug('Enabling private project middleware');
|
||||
@ -27,10 +27,9 @@ const privateProjectMiddleware = (
|
||||
return true;
|
||||
}
|
||||
const permissions = await accessService.getPermissionsForUser(user);
|
||||
|
||||
return (
|
||||
permissions.map((p) => p.permission).includes('ADMIN') ||
|
||||
projectService.isProjectUser(user.id, projectId)
|
||||
privateProjectChecker.hasAccessToProject(user.id, projectId)
|
||||
);
|
||||
};
|
||||
next();
|
||||
|
@ -15,27 +15,76 @@ class PrivateProjectStore implements IPrivateProjectStore {
|
||||
destroy(): void {}
|
||||
|
||||
async getUserAccessibleProjects(userId: number): Promise<string[]> {
|
||||
const projects = await this.db
|
||||
.from((db) => {
|
||||
db.select('project')
|
||||
.from('role_user')
|
||||
.leftJoin('roles', 'role_user.role_id', 'roles.id')
|
||||
.where('user_id', userId)
|
||||
.union((queryBuilder) => {
|
||||
queryBuilder
|
||||
.select('project')
|
||||
.from('group_role')
|
||||
.leftJoin(
|
||||
'group_user',
|
||||
'group_user.group_id',
|
||||
'group_role.group_id',
|
||||
)
|
||||
.where('user_id', userId);
|
||||
})
|
||||
.as('query');
|
||||
const isNotViewer = await this.db('role_user')
|
||||
.join('roles', 'role_user.role_id', 'roles.id')
|
||||
.where('role_user.user_id', userId)
|
||||
.andWhere((db) => {
|
||||
db.whereNot({
|
||||
'roles.name': 'Viewer',
|
||||
'roles.type': 'root',
|
||||
});
|
||||
})
|
||||
.pluck('project');
|
||||
return projects;
|
||||
.count('*')
|
||||
.first();
|
||||
|
||||
if (isNotViewer && isNotViewer.count > 0) {
|
||||
const allProjects = await this.db('projects').pluck('id');
|
||||
return allProjects;
|
||||
}
|
||||
|
||||
const accessibleProjects = await this.db
|
||||
.from((db) => {
|
||||
db.distinct('accessible_projects.project_id')
|
||||
.select('projects.id as project_id')
|
||||
.from('projects')
|
||||
.leftJoin(
|
||||
'project_settings',
|
||||
'projects.id',
|
||||
'project_settings.project',
|
||||
)
|
||||
.where('project_settings.project_mode', '!=', 'private')
|
||||
.unionAll((queryBuilder) => {
|
||||
queryBuilder
|
||||
.select('projects.id as project_id')
|
||||
.from('projects')
|
||||
.join(
|
||||
'project_settings',
|
||||
'projects.id',
|
||||
'project_settings.project',
|
||||
)
|
||||
.where(
|
||||
'project_settings.project_mode',
|
||||
'=',
|
||||
'private',
|
||||
)
|
||||
.whereIn('projects.id', (whereBuilder) => {
|
||||
whereBuilder
|
||||
.select('role_user.project')
|
||||
.from('role_user')
|
||||
.leftJoin(
|
||||
'roles',
|
||||
'role_user.role_id',
|
||||
'roles.id',
|
||||
)
|
||||
.where('role_user.user_id', userId);
|
||||
})
|
||||
.orWhereIn('projects.id', (whereBuilder) => {
|
||||
whereBuilder
|
||||
.select('group_role.project')
|
||||
.from('group_role')
|
||||
.leftJoin(
|
||||
'group_user',
|
||||
'group_user.group_id',
|
||||
'group_role.group_id',
|
||||
)
|
||||
.where('group_user.user_id', userId);
|
||||
});
|
||||
})
|
||||
.as('accessible_projects');
|
||||
})
|
||||
.select('*');
|
||||
|
||||
return accessibleProjects;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,6 @@ import ProjectStore from '../../db/project-store';
|
||||
import FeatureToggleStore from '../../db/feature-toggle-store';
|
||||
import FeatureTypeStore from '../../db/feature-type-store';
|
||||
import { FeatureEnvironmentStore } from '../../db/feature-environment-store';
|
||||
import FeatureTagStore from '../../db/feature-tag-store';
|
||||
import ProjectStatsStore from '../../db/project-stats-store';
|
||||
import {
|
||||
createAccessService,
|
||||
@ -32,11 +31,14 @@ import FakeFeatureToggleStore from '../../../test/fixtures/fake-feature-toggle-s
|
||||
import FakeFeatureTypeStore from '../../../test/fixtures/fake-feature-type-store';
|
||||
import FakeEnvironmentStore from '../../../test/fixtures/fake-environment-store';
|
||||
import FakeFeatureEnvironmentStore from '../../../test/fixtures/fake-feature-environment-store';
|
||||
import FakeFeatureTagStore from '../../../test/fixtures/fake-feature-tag-store';
|
||||
import FakeProjectStatsStore from '../../../test/fixtures/fake-project-stats-store';
|
||||
import FakeFavoriteFeaturesStore from '../../../test/fixtures/fake-favorite-features-store';
|
||||
import FakeFavoriteProjectsStore from '../../../test/fixtures/fake-favorite-projects-store';
|
||||
import { FakeAccountStore } from '../../../test/fixtures/fake-account-store';
|
||||
import {
|
||||
createFakePrivateProjectChecker,
|
||||
createPrivateProjectChecker,
|
||||
} from '../private-project/createPrivateProjectChecker';
|
||||
|
||||
export const createProjectService = (
|
||||
db: Db,
|
||||
@ -60,7 +62,6 @@ export const createProjectService = (
|
||||
eventBus,
|
||||
getLogger,
|
||||
);
|
||||
const featureTagStore = new FeatureTagStore(db, eventBus, getLogger);
|
||||
const projectStatsStore = new ProjectStatsStore(db, eventBus, getLogger);
|
||||
const accessService: AccessService = createAccessService(db, config);
|
||||
const featureToggleService = createFeatureToggleService(db, config);
|
||||
@ -87,6 +88,8 @@ export const createProjectService = (
|
||||
{ getLogger },
|
||||
);
|
||||
|
||||
const privateProjectChecker = createPrivateProjectChecker(db, config);
|
||||
|
||||
return new ProjectService(
|
||||
{
|
||||
projectStore,
|
||||
@ -95,7 +98,6 @@ export const createProjectService = (
|
||||
featureTypeStore,
|
||||
environmentStore,
|
||||
featureEnvironmentStore,
|
||||
featureTagStore,
|
||||
accountStore,
|
||||
projectStatsStore,
|
||||
},
|
||||
@ -104,6 +106,7 @@ export const createProjectService = (
|
||||
featureToggleService,
|
||||
groupService,
|
||||
favoriteService,
|
||||
privateProjectChecker,
|
||||
);
|
||||
};
|
||||
|
||||
@ -119,7 +122,6 @@ export const createFakeProjectService = (
|
||||
const accountStore = new FakeAccountStore();
|
||||
const environmentStore = new FakeEnvironmentStore();
|
||||
const featureEnvironmentStore = new FakeFeatureEnvironmentStore();
|
||||
const featureTagStore = new FakeFeatureTagStore();
|
||||
const projectStatsStore = new FakeProjectStatsStore();
|
||||
const accessService = createFakeAccessService(config);
|
||||
const featureToggleService = createFakeFeatureToggleService(config);
|
||||
@ -138,6 +140,8 @@ export const createFakeProjectService = (
|
||||
{ getLogger },
|
||||
);
|
||||
|
||||
const privateProjectChecker = createFakePrivateProjectChecker();
|
||||
|
||||
return new ProjectService(
|
||||
{
|
||||
projectStore,
|
||||
@ -146,7 +150,6 @@ export const createFakeProjectService = (
|
||||
featureTypeStore,
|
||||
environmentStore,
|
||||
featureEnvironmentStore,
|
||||
featureTagStore,
|
||||
accountStore,
|
||||
projectStatsStore,
|
||||
},
|
||||
@ -155,5 +158,6 @@ export const createFakeProjectService = (
|
||||
featureToggleService,
|
||||
groupService,
|
||||
favoriteService,
|
||||
privateProjectChecker,
|
||||
);
|
||||
};
|
||||
|
@ -122,11 +122,13 @@ export default class ArchiveController extends Controller {
|
||||
}
|
||||
|
||||
async getArchivedFeatures(
|
||||
req: Request,
|
||||
req: IAuthRequest,
|
||||
res: Response<FeaturesSchema>,
|
||||
): Promise<void> {
|
||||
const { user } = req;
|
||||
const features = await this.featureService.getMetadataForAllFeatures(
|
||||
true,
|
||||
user.id,
|
||||
);
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
|
@ -61,7 +61,6 @@ const checkPrivateProjectPermissions = () => async (req, res, next) => {
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return res.status(404).end();
|
||||
};
|
||||
|
||||
|
@ -1854,8 +1854,17 @@ class FeatureToggleService {
|
||||
|
||||
async getMetadataForAllFeatures(
|
||||
archived: boolean,
|
||||
userId: number,
|
||||
): Promise<FeatureToggle[]> {
|
||||
return this.featureToggleStore.getAll({ archived });
|
||||
const features = await this.featureToggleStore.getAll({ archived });
|
||||
if (this.flagResolver.isEnabled('privateProjects')) {
|
||||
const projects =
|
||||
await this.privateProjectChecker.getUserAccessibleProjects(
|
||||
userId,
|
||||
);
|
||||
return features.filter((f) => projects.includes(f.project));
|
||||
}
|
||||
return features;
|
||||
}
|
||||
|
||||
async getMetadataForAllFeaturesByProjectId(
|
||||
|
@ -61,7 +61,7 @@ import { createFeatureToggleService } from '../features';
|
||||
import EventAnnouncerService from './event-announcer-service';
|
||||
import { createGroupService } from '../features/group/createGroupService';
|
||||
import {
|
||||
createFakeprivateProjectChecker,
|
||||
createFakePrivateProjectChecker,
|
||||
createPrivateProjectChecker,
|
||||
} from '../features/private-project/createPrivateProjectChecker';
|
||||
|
||||
@ -190,7 +190,7 @@ export const createServices = (
|
||||
);
|
||||
const privateProjectChecker = db
|
||||
? createPrivateProjectChecker(db, config)
|
||||
: createFakeprivateProjectChecker();
|
||||
: createFakePrivateProjectChecker();
|
||||
const featureToggleServiceV2 = new FeatureToggleService(
|
||||
stores,
|
||||
config,
|
||||
@ -209,6 +209,7 @@ export const createServices = (
|
||||
featureToggleServiceV2,
|
||||
groupService,
|
||||
favoritesService,
|
||||
privateProjectChecker,
|
||||
);
|
||||
const projectHealthService = new ProjectHealthService(
|
||||
stores,
|
||||
@ -323,6 +324,7 @@ export const createServices = (
|
||||
configurationRevisionService,
|
||||
transactionalFeatureToggleService,
|
||||
transactionalGroupService,
|
||||
privateProjectChecker,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -48,7 +48,6 @@ import {
|
||||
} from '../types/stores/access-store';
|
||||
import FeatureToggleService from './feature-toggle-service';
|
||||
import IncompatibleProjectError from '../error/incompatible-project-error';
|
||||
import { IFeatureTagStore } from 'lib/types/stores/feature-tag-store';
|
||||
import ProjectWithoutOwnerError from '../error/project-without-owner-error';
|
||||
import { arraysHaveSameItems } from '../util';
|
||||
import { GroupService } from './group-service';
|
||||
@ -60,6 +59,7 @@ import { uniqueByKey } from '../util/unique';
|
||||
import { BadDataError, PermissionError } from '../error';
|
||||
import { ProjectDoraMetricsSchema } from 'lib/openapi';
|
||||
import { checkFeatureNamingData } from '../features/feature-naming-pattern/feature-naming-validation';
|
||||
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
||||
|
||||
const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown';
|
||||
|
||||
@ -103,7 +103,7 @@ export default class ProjectService {
|
||||
|
||||
private featureToggleService: FeatureToggleService;
|
||||
|
||||
private tagStore: IFeatureTagStore;
|
||||
private privateProjectChecker: IPrivateProjectChecker;
|
||||
|
||||
private accountStore: IAccountStore;
|
||||
|
||||
@ -121,7 +121,6 @@ export default class ProjectService {
|
||||
featureTypeStore,
|
||||
environmentStore,
|
||||
featureEnvironmentStore,
|
||||
featureTagStore,
|
||||
accountStore,
|
||||
projectStatsStore,
|
||||
}: Pick<
|
||||
@ -132,7 +131,6 @@ export default class ProjectService {
|
||||
| 'featureTypeStore'
|
||||
| 'environmentStore'
|
||||
| 'featureEnvironmentStore'
|
||||
| 'featureTagStore'
|
||||
| 'accountStore'
|
||||
| 'projectStatsStore'
|
||||
>,
|
||||
@ -141,6 +139,7 @@ export default class ProjectService {
|
||||
featureToggleService: FeatureToggleService,
|
||||
groupService: GroupService,
|
||||
favoriteService: FavoritesService,
|
||||
privateProjectChecker: IPrivateProjectChecker,
|
||||
) {
|
||||
this.store = projectStore;
|
||||
this.environmentStore = environmentStore;
|
||||
@ -151,7 +150,7 @@ export default class ProjectService {
|
||||
this.featureTypeStore = featureTypeStore;
|
||||
this.featureToggleService = featureToggleService;
|
||||
this.favoritesService = favoriteService;
|
||||
this.tagStore = featureTagStore;
|
||||
this.privateProjectChecker = privateProjectChecker;
|
||||
this.accountStore = accountStore;
|
||||
this.groupService = groupService;
|
||||
this.projectStatsStore = projectStatsStore;
|
||||
@ -163,7 +162,17 @@ export default class ProjectService {
|
||||
query?: IProjectQuery,
|
||||
userId?: number,
|
||||
): Promise<IProjectWithCount[]> {
|
||||
return this.store.getProjectsWithCounts(query, userId);
|
||||
const projects = await this.store.getProjectsWithCounts(query, userId);
|
||||
if (this.flagResolver.isEnabled('privateProjects') && userId) {
|
||||
const accessibleProjects =
|
||||
await this.privateProjectChecker.getUserAccessibleProjects(
|
||||
userId,
|
||||
);
|
||||
return projects.filter((project) =>
|
||||
accessibleProjects.includes(project.id),
|
||||
);
|
||||
}
|
||||
return projects;
|
||||
}
|
||||
|
||||
async getProject(id: string): Promise<IProject> {
|
||||
|
@ -43,6 +43,7 @@ import ExportImportService from '../features/export-import-toggles/export-import
|
||||
import { ISegmentService } from '../segments/segment-service-interface';
|
||||
import ConfigurationRevisionService from '../features/feature-toggle/configuration-revision-service';
|
||||
import EventAnnouncerService from 'lib/services/event-announcer-service';
|
||||
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
||||
|
||||
export interface IUnleashServices {
|
||||
accessService: AccessService;
|
||||
@ -97,4 +98,5 @@ export interface IUnleashServices {
|
||||
db: Knex.Transaction,
|
||||
) => FeatureToggleService;
|
||||
transactionalGroupService: (db: Knex.Transaction) => GroupService;
|
||||
privateProjectChecker: IPrivateProjectChecker;
|
||||
}
|
||||
|
@ -265,6 +265,7 @@ beforeAll(async () => {
|
||||
featureToggleService,
|
||||
groupService,
|
||||
favoritesService,
|
||||
privateProjectChecker,
|
||||
);
|
||||
|
||||
editorUser = await createUser(editorRole.id);
|
||||
|
@ -63,6 +63,7 @@ beforeAll(async () => {
|
||||
featureToggleService,
|
||||
groupService,
|
||||
favoritesService,
|
||||
privateProjectChecker,
|
||||
);
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
|
@ -58,6 +58,7 @@ beforeAll(async () => {
|
||||
featureToggleService,
|
||||
groupService,
|
||||
favoritesService,
|
||||
privateProjectChecker,
|
||||
);
|
||||
projectHealthService = new ProjectHealthService(
|
||||
stores,
|
||||
|
@ -80,6 +80,7 @@ beforeAll(async () => {
|
||||
featureToggleService,
|
||||
groupService,
|
||||
favoritesService,
|
||||
privateProjectChecker,
|
||||
);
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user