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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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