Skip to content

Commit

Permalink
feat: private project filtering and store implementation (#4758)
Browse files Browse the repository at this point in the history
  • Loading branch information
sjaanus committed Sep 18, 2023
1 parent 2928857 commit 39d2d06
Show file tree
Hide file tree
Showing 18 changed files with 138 additions and 43 deletions.
4 changes: 2 additions & 2 deletions src/lib/features/feature-toggle/createFeatureToggleService.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -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,
Expand Down
@@ -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,
Expand All @@ -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.');
}
}
9 changes: 9 additions & 0 deletions src/lib/features/private-project/privateProjectChecker.ts
Expand Up @@ -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>;
}
5 changes: 2 additions & 3 deletions src/lib/features/private-project/privateProjectMiddleware.ts
Expand Up @@ -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');
Expand All @@ -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();
Expand Down
81 changes: 65 additions & 16 deletions src/lib/features/private-project/privateProjectStore.ts
Expand Up @@ -15,27 +15,76 @@ class PrivateProjectStore implements IPrivateProjectStore {
destroy(): void {}

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) => {
db.select('project')
.from('role_user')
.leftJoin('roles', 'role_user.role_id', 'roles.id')
.where('user_id', userId)
.union((queryBuilder) => {
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('project')
.from('group_role')
.leftJoin(
'group_user',
'group_user.group_id',
'group_role.group_id',
.select('projects.id as project_id')
.from('projects')
.join(
'project_settings',
'projects.id',
'project_settings.project',
)
.where('user_id', userId);
.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('query');
.as('accessible_projects');
})
.pluck('project');
return projects;
.select('*');

return accessibleProjects;
}
}

Expand Down
16 changes: 10 additions & 6 deletions src/lib/features/project/createProjectService.ts
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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);
Expand All @@ -87,6 +88,8 @@ export const createProjectService = (
{ getLogger },
);

const privateProjectChecker = createPrivateProjectChecker(db, config);

return new ProjectService(
{
projectStore,
Expand All @@ -95,7 +98,6 @@ export const createProjectService = (
featureTypeStore,
environmentStore,
featureEnvironmentStore,
featureTagStore,
accountStore,
projectStatsStore,
},
Expand All @@ -104,6 +106,7 @@ export const createProjectService = (
featureToggleService,
groupService,
favoriteService,
privateProjectChecker,
);
};

Expand All @@ -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);
Expand All @@ -138,6 +140,8 @@ export const createFakeProjectService = (
{ getLogger },
);

const privateProjectChecker = createFakePrivateProjectChecker();

return new ProjectService(
{
projectStore,
Expand All @@ -146,7 +150,6 @@ export const createFakeProjectService = (
featureTypeStore,
environmentStore,
featureEnvironmentStore,
featureTagStore,
accountStore,
projectStatsStore,
},
Expand All @@ -155,5 +158,6 @@ export const createFakeProjectService = (
featureToggleService,
groupService,
favoriteService,
privateProjectChecker,
);
};
4 changes: 3 additions & 1 deletion src/lib/routes/admin-api/archive.ts
Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion src/lib/routes/controller.ts
Expand Up @@ -61,7 +61,6 @@ const checkPrivateProjectPermissions = () => async (req, res, next) => {
) {
return next();
}

return res.status(404).end();
};

Expand Down
11 changes: 10 additions & 1 deletion src/lib/services/feature-toggle-service.ts
Expand Up @@ -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(
Expand Down
6 changes: 4 additions & 2 deletions src/lib/services/index.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -190,7 +190,7 @@ export const createServices = (
);
const privateProjectChecker = db
? createPrivateProjectChecker(db, config)
: createFakeprivateProjectChecker();
: createFakePrivateProjectChecker();
const featureToggleServiceV2 = new FeatureToggleService(
stores,
config,
Expand All @@ -209,6 +209,7 @@ export const createServices = (
featureToggleServiceV2,
groupService,
favoritesService,
privateProjectChecker,
);
const projectHealthService = new ProjectHealthService(
stores,
Expand Down Expand Up @@ -323,6 +324,7 @@ export const createServices = (
configurationRevisionService,
transactionalFeatureToggleService,
transactionalGroupService,
privateProjectChecker,
};
};

Expand Down

0 comments on commit 39d2d06

Please sign in to comment.