From 10afbc8a9e43e4ae6f7f67bdb18bc350b4da777c Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Thu, 14 Sep 2023 11:43:39 +0200 Subject: [PATCH] feat: add service method to retrieve group and project access for all users (#4708) --- src/lib/db/access-store.ts | 38 ++++++++++- src/lib/services/access-service.ts | 6 +- src/lib/types/model.ts | 11 ++++ src/lib/types/stores/access-store.ts | 3 +- .../e2e/services/access-service.e2e.test.ts | 63 +++++++++++++++++++ src/test/fixtures/fake-access-store.ts | 6 +- 6 files changed, 123 insertions(+), 4 deletions(-) diff --git a/src/lib/db/access-store.ts b/src/lib/db/access-store.ts index 7008cc8caa..187f466862 100644 --- a/src/lib/db/access-store.ts +++ b/src/lib/db/access-store.ts @@ -12,7 +12,7 @@ import { IUserRole, IUserWithProjectRoles, } from '../types/stores/access-store'; -import { IPermission } from '../types/model'; +import { IPermission, IUserAccessOverview } from '../types/model'; import NotFoundError from '../error/notfound-error'; import { ENVIRONMENT_PERMISSION_TYPE, @@ -758,4 +758,40 @@ export class AccessStore implements IAccessStore { [destinationEnvironment, sourceEnvironment], ); } + + async getUserAccessOverview(): Promise { + const result = await this.db + .raw(`SELECT u.id, u.created_at, u.name, u.email, u.seen_at, up.p_array as projects, gr.p_array as groups, r.name as root_role + FROM users u, LATERAL ( + SELECT ARRAY ( + SELECT ru.project + FROM role_user ru + WHERE ru.user_id = u.id + ) AS p_array + ) up, LATERAL ( + SELECT r.name + FROM role_user ru + inner join roles r on ru.role_id = r.id + where ru.user_id = u.id and r.type='root' + ) r, LATERAL ( + SELECT ARRAY ( + select g.name from group_user gu + left join groups g on g.id = gu.group_id + WHERE gu.user_id = u.id + ) AS p_array + ) gr + order by u.id;`); + return result.rows.map((row) => { + return { + userId: row.id, + createdAt: row.created_at, + userName: row.name, + userEmail: row.email, + lastSeen: row.seen_at, + accessibleProjects: row.projects, + groups: row.groups, + rootRole: row.root_role, + }; + }); + } } diff --git a/src/lib/services/access-service.ts b/src/lib/services/access-service.ts index b7ab159e1c..2286aa41e6 100644 --- a/src/lib/services/access-service.ts +++ b/src/lib/services/access-service.ts @@ -40,7 +40,7 @@ import InvalidOperationError from '../error/invalid-operation-error'; import BadDataError from '../error/bad-data-error'; import { IGroup } from '../types/group'; import { GroupService } from './group-service'; -import { IFlagResolver, IUnleashConfig } from 'lib/types'; +import { IFlagResolver, IUnleashConfig, IUserAccessOverview } from 'lib/types'; const { ADMIN } = permissions; @@ -736,4 +736,8 @@ export class AccessService { await this.validateRoleIsUnique(role.name, existingId); return cleanedRole; } + + async getUserAccessOverview(): Promise { + return this.store.getUserAccessOverview(); + } } diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 7a1b74e849..ac378d92a9 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -459,3 +459,14 @@ export interface IFeatureStrategySegment { featureStrategyId: string; segmentId: number; } + +export interface IUserAccessOverview { + userId: number; + createdAt?: Date; + userName?: string; + userEmail: number; + lastSeen?: Date; + accessibleProjects: string[]; + groups: string[]; + rootRole: string; +} diff --git a/src/lib/types/stores/access-store.ts b/src/lib/types/stores/access-store.ts index 875e4519a9..bd1f0c9126 100644 --- a/src/lib/types/stores/access-store.ts +++ b/src/lib/types/stores/access-store.ts @@ -1,6 +1,6 @@ import { PermissionRef } from 'lib/services/access-service'; import { IGroupModelWithProjectRole } from '../group'; -import { IPermission, IUserWithRole } from '../model'; +import { IPermission, IUserAccessOverview, IUserWithRole } from '../model'; import { Store } from './store'; export interface IUserPermission { @@ -200,4 +200,5 @@ export interface IAccessStore extends Store { ): Promise; removeUserAccess(projectId: string, userId: number): Promise; removeGroupAccess(projectId: string, groupId: number): Promise; + getUserAccessOverview(): Promise; } diff --git a/src/test/e2e/services/access-service.e2e.test.ts b/src/test/e2e/services/access-service.e2e.test.ts index a959b82d73..e04c5046e7 100644 --- a/src/test/e2e/services/access-service.e2e.test.ts +++ b/src/test/e2e/services/access-service.e2e.test.ts @@ -10,6 +10,7 @@ import { ICreateGroupUserModel, IPermission, IUnleashStores, + IUserAccessOverview, } from '../../../lib/types'; import FeatureToggleService from '../../../lib/services/feature-toggle-service'; import ProjectService from '../../../lib/services/project-service'; @@ -1851,3 +1852,65 @@ test('remove group access should remove all project roles, while leaving root ro expect(newAssignedPermissions.length).toBe(1); expect(newAssignedPermissions[0].permission).toBe(permissions.ADMIN); }); + +test('access overview should have admin access and default project for admin user', async () => { + const email = 'a-person@places.com'; + + const { userStore } = stores; + const user = await userStore.insert({ + name: 'Some User', + email, + }); + + await accessService.setUserRootRole(user.id, adminRole.id); + + const accessOverView: IUserAccessOverview[] = + await accessService.getUserAccessOverview(); + const userAccess = accessOverView.find( + (overviewRow) => overviewRow.userId == user.id, + )!; + + expect(userAccess.userId).toBe(user.id); + + expect(userAccess.rootRole).toBe('Admin'); + expect(userAccess.accessibleProjects).toStrictEqual(['default']); +}); + +test('access overview should have group access for groups that they are in', async () => { + const email = 'a-nother-person@places.com'; + + const { userStore } = stores; + const user = await userStore.insert({ + name: 'Some Other User', + email, + }); + + await accessService.setUserRootRole(user.id, adminRole.id); + + const group = await stores.groupStore.create({ + name: 'Test Group', + }); + + await stores.groupStore.addUsersToGroup( + group.id, + [ + { + user: { + id: user.id, + }, + }, + ], + 'Admin', + ); + + const accessOverView: IUserAccessOverview[] = + await accessService.getUserAccessOverview(); + const userAccess = accessOverView.find( + (overviewRow) => overviewRow.userId == user.id, + )!; + + expect(userAccess.userId).toBe(user.id); + + expect(userAccess.rootRole).toBe('Admin'); + expect(userAccess.groups).toStrictEqual(['Test Group']); +}); diff --git a/src/test/fixtures/fake-access-store.ts b/src/test/fixtures/fake-access-store.ts index bc5242a57f..19ef6619df 100644 --- a/src/test/fixtures/fake-access-store.ts +++ b/src/test/fixtures/fake-access-store.ts @@ -10,7 +10,7 @@ import { IUserWithProjectRoles, } from '../../lib/types/stores/access-store'; import { IPermission } from 'lib/types/model'; -import { IRoleStore } from 'lib/types'; +import { IRoleStore, IUserAccessOverview } from 'lib/types'; import FakeRoleStore from './fake-role-store'; import { PermissionRef } from 'lib/services/access-service'; @@ -289,6 +289,10 @@ class AccessStoreMock implements IAccessStore { removeGroupAccess(projectId: string, groupId: number): Promise { throw new Error('Method not implemented.'); } + + getUserAccessOverview(): Promise { + throw new Error('Method not implemented.'); + } } module.exports = AccessStoreMock;