1
0
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:
Jaanus Sellin 2023-09-18 11:06:26 +03:00 committed by GitHub
parent 2928857857
commit 39d2d065cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 142 additions and 47 deletions

View File

@ -42,7 +42,7 @@ import {
import StrategyStore from '../../db/strategy-store'; import StrategyStore from '../../db/strategy-store';
import FakeStrategiesStore from '../../../test/fixtures/fake-strategies-store'; import FakeStrategiesStore from '../../../test/fixtures/fake-strategies-store';
import { import {
createFakeprivateProjectChecker, createFakePrivateProjectChecker,
createPrivateProjectChecker, createPrivateProjectChecker,
} from '../private-project/createPrivateProjectChecker'; } from '../private-project/createPrivateProjectChecker';
@ -155,7 +155,7 @@ export const createFakeFeatureToggleService = (
); );
const segmentService = createFakeSegmentService(config); const segmentService = createFakeSegmentService(config);
const changeRequestAccessReadModel = createFakeChangeRequestAccessService(); const changeRequestAccessReadModel = createFakeChangeRequestAccessService();
const fakeprivateProjectChecker = createFakeprivateProjectChecker(); const fakeprivateProjectChecker = createFakePrivateProjectChecker();
const featureToggleService = new FeatureToggleService( const featureToggleService = new FeatureToggleService(
{ {
featureStrategiesStore, featureStrategiesStore,

View File

@ -1,7 +1,7 @@
import { Db, IUnleashConfig } from 'lib/server-impl'; import { Db, IUnleashConfig } from 'lib/server-impl';
import PrivateProjectStore from './privateProjectStore'; import PrivateProjectStore from './privateProjectStore';
import { PrivateProjectChecker } from './privateProjectChecker'; import { PrivateProjectChecker } from './privateProjectChecker';
import { FakeprivateProjectChecker } from './fakePrivateProjectChecker'; import { FakePrivateProjectChecker } from './fakePrivateProjectChecker';
export const createPrivateProjectChecker = ( export const createPrivateProjectChecker = (
db: Db, db: Db,
@ -15,7 +15,7 @@ export const createPrivateProjectChecker = (
}); });
}; };
export const createFakeprivateProjectChecker = export const createFakePrivateProjectChecker =
(): FakeprivateProjectChecker => { (): FakePrivateProjectChecker => {
return new FakeprivateProjectChecker(); return new FakePrivateProjectChecker();
}; };

View File

@ -1,8 +1,14 @@
import { IPrivateProjectChecker } from './privateProjectCheckerType'; 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 // eslint-disable-next-line @typescript-eslint/no-unused-vars
async getUserAccessibleProjects(userId: number): Promise<string[]> { async getUserAccessibleProjects(userId: number): Promise<string[]> {
throw new Error('Method not implemented.'); 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.');
}
} }

View File

@ -14,4 +14,13 @@ export class PrivateProjectChecker implements IPrivateProjectChecker {
async getUserAccessibleProjects(userId: number): Promise<string[]> { async getUserAccessibleProjects(userId: number): Promise<string[]> {
return this.privateProjectStore.getUserAccessibleProjects(userId); return this.privateProjectStore.getUserAccessibleProjects(userId);
} }
async hasAccessToProject(
userId: number,
projectId: string,
): Promise<boolean> {
return (await this.getUserAccessibleProjects(userId)).includes(
projectId,
);
}
} }

View File

@ -1,3 +1,4 @@
export interface IPrivateProjectChecker { export interface IPrivateProjectChecker {
getUserAccessibleProjects(userId: number): Promise<string[]>; getUserAccessibleProjects(userId: number): Promise<string[]>;
hasAccessToProject(userId: number, projectId: string): Promise<boolean>;
} }

View File

@ -7,7 +7,7 @@ const privateProjectMiddleware = (
getLogger, getLogger,
flagResolver, flagResolver,
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>, }: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
{ projectService, accessService }: IUnleashServices, { accessService, privateProjectChecker }: IUnleashServices,
): any => { ): any => {
const logger = getLogger('/middleware/project-middleware.ts'); const logger = getLogger('/middleware/project-middleware.ts');
logger.debug('Enabling private project middleware'); logger.debug('Enabling private project middleware');
@ -27,10 +27,9 @@ const privateProjectMiddleware = (
return true; return true;
} }
const permissions = await accessService.getPermissionsForUser(user); const permissions = await accessService.getPermissionsForUser(user);
return ( return (
permissions.map((p) => p.permission).includes('ADMIN') || permissions.map((p) => p.permission).includes('ADMIN') ||
projectService.isProjectUser(user.id, projectId) privateProjectChecker.hasAccessToProject(user.id, projectId)
); );
}; };
next(); next();

View File

@ -15,27 +15,76 @@ class PrivateProjectStore implements IPrivateProjectStore {
destroy(): void {} destroy(): void {}
async getUserAccessibleProjects(userId: number): Promise<string[]> { async getUserAccessibleProjects(userId: number): Promise<string[]> {
const projects = await this.db 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',
});
})
.count('*')
.first();
if (isNotViewer && isNotViewer.count > 0) {
const allProjects = await this.db('projects').pluck('id');
return allProjects;
}
const accessibleProjects = await this.db
.from((db) => { .from((db) => {
db.select('project') db.distinct('accessible_projects.project_id')
.from('role_user') .select('projects.id as project_id')
.leftJoin('roles', 'role_user.role_id', 'roles.id') .from('projects')
.where('user_id', userId) .leftJoin(
.union((queryBuilder) => { 'project_settings',
'projects.id',
'project_settings.project',
)
.where('project_settings.project_mode', '!=', 'private')
.unionAll((queryBuilder) => {
queryBuilder queryBuilder
.select('project') .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') .from('group_role')
.leftJoin( .leftJoin(
'group_user', 'group_user',
'group_user.group_id', 'group_user.group_id',
'group_role.group_id', 'group_role.group_id',
) )
.where('user_id', userId); .where('group_user.user_id', userId);
});
}) })
.as('query'); .as('accessible_projects');
}) })
.pluck('project'); .select('*');
return projects;
return accessibleProjects;
} }
} }

View File

@ -15,7 +15,6 @@ import ProjectStore from '../../db/project-store';
import FeatureToggleStore from '../../db/feature-toggle-store'; import FeatureToggleStore from '../../db/feature-toggle-store';
import FeatureTypeStore from '../../db/feature-type-store'; import FeatureTypeStore from '../../db/feature-type-store';
import { FeatureEnvironmentStore } from '../../db/feature-environment-store'; import { FeatureEnvironmentStore } from '../../db/feature-environment-store';
import FeatureTagStore from '../../db/feature-tag-store';
import ProjectStatsStore from '../../db/project-stats-store'; import ProjectStatsStore from '../../db/project-stats-store';
import { import {
createAccessService, createAccessService,
@ -32,11 +31,14 @@ import FakeFeatureToggleStore from '../../../test/fixtures/fake-feature-toggle-s
import FakeFeatureTypeStore from '../../../test/fixtures/fake-feature-type-store'; import FakeFeatureTypeStore from '../../../test/fixtures/fake-feature-type-store';
import FakeEnvironmentStore from '../../../test/fixtures/fake-environment-store'; import FakeEnvironmentStore from '../../../test/fixtures/fake-environment-store';
import FakeFeatureEnvironmentStore from '../../../test/fixtures/fake-feature-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 FakeProjectStatsStore from '../../../test/fixtures/fake-project-stats-store';
import FakeFavoriteFeaturesStore from '../../../test/fixtures/fake-favorite-features-store'; import FakeFavoriteFeaturesStore from '../../../test/fixtures/fake-favorite-features-store';
import FakeFavoriteProjectsStore from '../../../test/fixtures/fake-favorite-projects-store'; import FakeFavoriteProjectsStore from '../../../test/fixtures/fake-favorite-projects-store';
import { FakeAccountStore } from '../../../test/fixtures/fake-account-store'; import { FakeAccountStore } from '../../../test/fixtures/fake-account-store';
import {
createFakePrivateProjectChecker,
createPrivateProjectChecker,
} from '../private-project/createPrivateProjectChecker';
export const createProjectService = ( export const createProjectService = (
db: Db, db: Db,
@ -60,7 +62,6 @@ export const createProjectService = (
eventBus, eventBus,
getLogger, getLogger,
); );
const featureTagStore = new FeatureTagStore(db, eventBus, getLogger);
const projectStatsStore = new ProjectStatsStore(db, eventBus, getLogger); const projectStatsStore = new ProjectStatsStore(db, eventBus, getLogger);
const accessService: AccessService = createAccessService(db, config); const accessService: AccessService = createAccessService(db, config);
const featureToggleService = createFeatureToggleService(db, config); const featureToggleService = createFeatureToggleService(db, config);
@ -87,6 +88,8 @@ export const createProjectService = (
{ getLogger }, { getLogger },
); );
const privateProjectChecker = createPrivateProjectChecker(db, config);
return new ProjectService( return new ProjectService(
{ {
projectStore, projectStore,
@ -95,7 +98,6 @@ export const createProjectService = (
featureTypeStore, featureTypeStore,
environmentStore, environmentStore,
featureEnvironmentStore, featureEnvironmentStore,
featureTagStore,
accountStore, accountStore,
projectStatsStore, projectStatsStore,
}, },
@ -104,6 +106,7 @@ export const createProjectService = (
featureToggleService, featureToggleService,
groupService, groupService,
favoriteService, favoriteService,
privateProjectChecker,
); );
}; };
@ -119,7 +122,6 @@ export const createFakeProjectService = (
const accountStore = new FakeAccountStore(); const accountStore = new FakeAccountStore();
const environmentStore = new FakeEnvironmentStore(); const environmentStore = new FakeEnvironmentStore();
const featureEnvironmentStore = new FakeFeatureEnvironmentStore(); const featureEnvironmentStore = new FakeFeatureEnvironmentStore();
const featureTagStore = new FakeFeatureTagStore();
const projectStatsStore = new FakeProjectStatsStore(); const projectStatsStore = new FakeProjectStatsStore();
const accessService = createFakeAccessService(config); const accessService = createFakeAccessService(config);
const featureToggleService = createFakeFeatureToggleService(config); const featureToggleService = createFakeFeatureToggleService(config);
@ -138,6 +140,8 @@ export const createFakeProjectService = (
{ getLogger }, { getLogger },
); );
const privateProjectChecker = createFakePrivateProjectChecker();
return new ProjectService( return new ProjectService(
{ {
projectStore, projectStore,
@ -146,7 +150,6 @@ export const createFakeProjectService = (
featureTypeStore, featureTypeStore,
environmentStore, environmentStore,
featureEnvironmentStore, featureEnvironmentStore,
featureTagStore,
accountStore, accountStore,
projectStatsStore, projectStatsStore,
}, },
@ -155,5 +158,6 @@ export const createFakeProjectService = (
featureToggleService, featureToggleService,
groupService, groupService,
favoriteService, favoriteService,
privateProjectChecker,
); );
}; };

View File

@ -122,11 +122,13 @@ export default class ArchiveController extends Controller {
} }
async getArchivedFeatures( async getArchivedFeatures(
req: Request, req: IAuthRequest,
res: Response<FeaturesSchema>, res: Response<FeaturesSchema>,
): Promise<void> { ): Promise<void> {
const { user } = req;
const features = await this.featureService.getMetadataForAllFeatures( const features = await this.featureService.getMetadataForAllFeatures(
true, true,
user.id,
); );
this.openApiService.respondWithValidation( this.openApiService.respondWithValidation(
200, 200,

View File

@ -61,7 +61,6 @@ const checkPrivateProjectPermissions = () => async (req, res, next) => {
) { ) {
return next(); return next();
} }
return res.status(404).end(); return res.status(404).end();
}; };

View File

@ -1854,8 +1854,17 @@ class FeatureToggleService {
async getMetadataForAllFeatures( async getMetadataForAllFeatures(
archived: boolean, archived: boolean,
userId: number,
): Promise<FeatureToggle[]> { ): 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( async getMetadataForAllFeaturesByProjectId(

View File

@ -61,7 +61,7 @@ import { createFeatureToggleService } from '../features';
import EventAnnouncerService from './event-announcer-service'; import EventAnnouncerService from './event-announcer-service';
import { createGroupService } from '../features/group/createGroupService'; import { createGroupService } from '../features/group/createGroupService';
import { import {
createFakeprivateProjectChecker, createFakePrivateProjectChecker,
createPrivateProjectChecker, createPrivateProjectChecker,
} from '../features/private-project/createPrivateProjectChecker'; } from '../features/private-project/createPrivateProjectChecker';
@ -190,7 +190,7 @@ export const createServices = (
); );
const privateProjectChecker = db const privateProjectChecker = db
? createPrivateProjectChecker(db, config) ? createPrivateProjectChecker(db, config)
: createFakeprivateProjectChecker(); : createFakePrivateProjectChecker();
const featureToggleServiceV2 = new FeatureToggleService( const featureToggleServiceV2 = new FeatureToggleService(
stores, stores,
config, config,
@ -209,6 +209,7 @@ export const createServices = (
featureToggleServiceV2, featureToggleServiceV2,
groupService, groupService,
favoritesService, favoritesService,
privateProjectChecker,
); );
const projectHealthService = new ProjectHealthService( const projectHealthService = new ProjectHealthService(
stores, stores,
@ -323,6 +324,7 @@ export const createServices = (
configurationRevisionService, configurationRevisionService,
transactionalFeatureToggleService, transactionalFeatureToggleService,
transactionalGroupService, transactionalGroupService,
privateProjectChecker,
}; };
}; };

View File

@ -48,7 +48,6 @@ import {
} from '../types/stores/access-store'; } from '../types/stores/access-store';
import FeatureToggleService from './feature-toggle-service'; import FeatureToggleService from './feature-toggle-service';
import IncompatibleProjectError from '../error/incompatible-project-error'; 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 ProjectWithoutOwnerError from '../error/project-without-owner-error';
import { arraysHaveSameItems } from '../util'; import { arraysHaveSameItems } from '../util';
import { GroupService } from './group-service'; import { GroupService } from './group-service';
@ -60,6 +59,7 @@ import { uniqueByKey } from '../util/unique';
import { BadDataError, PermissionError } from '../error'; import { BadDataError, PermissionError } from '../error';
import { ProjectDoraMetricsSchema } from 'lib/openapi'; import { ProjectDoraMetricsSchema } from 'lib/openapi';
import { checkFeatureNamingData } from '../features/feature-naming-pattern/feature-naming-validation'; 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'; const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown';
@ -103,7 +103,7 @@ export default class ProjectService {
private featureToggleService: FeatureToggleService; private featureToggleService: FeatureToggleService;
private tagStore: IFeatureTagStore; private privateProjectChecker: IPrivateProjectChecker;
private accountStore: IAccountStore; private accountStore: IAccountStore;
@ -121,7 +121,6 @@ export default class ProjectService {
featureTypeStore, featureTypeStore,
environmentStore, environmentStore,
featureEnvironmentStore, featureEnvironmentStore,
featureTagStore,
accountStore, accountStore,
projectStatsStore, projectStatsStore,
}: Pick< }: Pick<
@ -132,7 +131,6 @@ export default class ProjectService {
| 'featureTypeStore' | 'featureTypeStore'
| 'environmentStore' | 'environmentStore'
| 'featureEnvironmentStore' | 'featureEnvironmentStore'
| 'featureTagStore'
| 'accountStore' | 'accountStore'
| 'projectStatsStore' | 'projectStatsStore'
>, >,
@ -141,6 +139,7 @@ export default class ProjectService {
featureToggleService: FeatureToggleService, featureToggleService: FeatureToggleService,
groupService: GroupService, groupService: GroupService,
favoriteService: FavoritesService, favoriteService: FavoritesService,
privateProjectChecker: IPrivateProjectChecker,
) { ) {
this.store = projectStore; this.store = projectStore;
this.environmentStore = environmentStore; this.environmentStore = environmentStore;
@ -151,7 +150,7 @@ export default class ProjectService {
this.featureTypeStore = featureTypeStore; this.featureTypeStore = featureTypeStore;
this.featureToggleService = featureToggleService; this.featureToggleService = featureToggleService;
this.favoritesService = favoriteService; this.favoritesService = favoriteService;
this.tagStore = featureTagStore; this.privateProjectChecker = privateProjectChecker;
this.accountStore = accountStore; this.accountStore = accountStore;
this.groupService = groupService; this.groupService = groupService;
this.projectStatsStore = projectStatsStore; this.projectStatsStore = projectStatsStore;
@ -163,7 +162,17 @@ export default class ProjectService {
query?: IProjectQuery, query?: IProjectQuery,
userId?: number, userId?: number,
): Promise<IProjectWithCount[]> { ): 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> { async getProject(id: string): Promise<IProject> {

View File

@ -43,6 +43,7 @@ import ExportImportService from '../features/export-import-toggles/export-import
import { ISegmentService } from '../segments/segment-service-interface'; import { ISegmentService } from '../segments/segment-service-interface';
import ConfigurationRevisionService from '../features/feature-toggle/configuration-revision-service'; import ConfigurationRevisionService from '../features/feature-toggle/configuration-revision-service';
import EventAnnouncerService from 'lib/services/event-announcer-service'; import EventAnnouncerService from 'lib/services/event-announcer-service';
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
export interface IUnleashServices { export interface IUnleashServices {
accessService: AccessService; accessService: AccessService;
@ -97,4 +98,5 @@ export interface IUnleashServices {
db: Knex.Transaction, db: Knex.Transaction,
) => FeatureToggleService; ) => FeatureToggleService;
transactionalGroupService: (db: Knex.Transaction) => GroupService; transactionalGroupService: (db: Knex.Transaction) => GroupService;
privateProjectChecker: IPrivateProjectChecker;
} }

View File

@ -265,6 +265,7 @@ beforeAll(async () => {
featureToggleService, featureToggleService,
groupService, groupService,
favoritesService, favoritesService,
privateProjectChecker,
); );
editorUser = await createUser(editorRole.id); editorUser = await createUser(editorRole.id);

View File

@ -63,6 +63,7 @@ beforeAll(async () => {
featureToggleService, featureToggleService,
groupService, groupService,
favoritesService, favoritesService,
privateProjectChecker,
); );
await projectService.createProject(project, user); await projectService.createProject(project, user);

View File

@ -58,6 +58,7 @@ beforeAll(async () => {
featureToggleService, featureToggleService,
groupService, groupService,
favoritesService, favoritesService,
privateProjectChecker,
); );
projectHealthService = new ProjectHealthService( projectHealthService = new ProjectHealthService(
stores, stores,

View File

@ -80,6 +80,7 @@ beforeAll(async () => {
featureToggleService, featureToggleService,
groupService, groupService,
favoritesService, favoritesService,
privateProjectChecker,
); );
}); });