diff --git a/src/lib/db/group-store.ts b/src/lib/db/group-store.ts index 476e9e43d7..732ebd113d 100644 --- a/src/lib/db/group-store.ts +++ b/src/lib/db/group-store.ts @@ -169,7 +169,7 @@ export default class GroupStore implements IGroupStore { return rowToGroup(row[0]); } - async addNewUsersToGroup( + async addUsersToGroup( groupId: number, users: IGroupUserModel[], userName: string, @@ -185,7 +185,7 @@ export default class GroupStore implements IGroupStore { return (transaction || this.db).batchInsert(T.GROUP_USER, rows); } - async deleteOldUsersFromGroup( + async deleteUsersFromGroup( deletableUsers: IGroupUser[], transaction?: Transaction, ): Promise { @@ -205,8 +205,65 @@ export default class GroupStore implements IGroupStore { userName: string, ): Promise { await this.db.transaction(async (tx) => { - await this.addNewUsersToGroup(groupId, newUsers, userName, tx); - await this.deleteOldUsersFromGroup(deletableUsers, tx); + await this.addUsersToGroup(groupId, newUsers, userName, tx); + await this.deleteUsersFromGroup(deletableUsers, tx); }); } + + async getNewGroupsForExternalUser( + userId: number, + externalGroups: string[], + ): Promise { + const rows = await this.db(`${T.GROUPS} as g`) + .leftJoin(`${T.GROUP_USER} as gs`, function () { + this.on('g.id', 'gs.group_id').andOnVal( + 'gs.user_id', + '=', + userId, + ); + }) + .where('gs.user_id', null) + .whereRaw('mappings_sso \\?| :groups', { groups: externalGroups }); + return rows.map(rowToGroup); + } + + async addUserToGroups( + userId: number, + groupIds: number[], + createdBy?: string, + ): Promise { + const rows = groupIds.map((groupId) => { + return { + group_id: groupId, + user_id: userId, + created_by: createdBy, + }; + }); + return this.db.batchInsert(T.GROUP_USER, rows); + } + + async getOldGroupsForExternalUser( + userId: number, + externalGroups: string[], + ): Promise { + const rows = await this.db(`${T.GROUP_USER} as gu`) + .leftJoin(`${T.GROUPS} as g`, 'g.id', 'gu.group_id') + .whereNotIn( + 'g.id', + this.db(T.GROUPS) + .select('id') + .whereRaw('mappings_sso \\?| :groups', { + groups: externalGroups, + }), + ) + .where('gu.user_id', userId); + return rows.map(rowToGroupUser); + } + + async getGroupsForUser(userId: number): Promise { + const rows = await this.db(T.GROUPS) + .leftJoin(T.GROUP_USER, 'groups.id', 'group_user.group_id') + .where('user_id', userId); + return rows.map(rowToGroup); + } } diff --git a/src/lib/services/group-service.ts b/src/lib/services/group-service.ts index 6ec7514dbd..6287a64b56 100644 --- a/src/lib/services/group-service.ts +++ b/src/lib/services/group-service.ts @@ -83,7 +83,7 @@ export class GroupService { const newGroup = await this.groupStore.create(group); - await this.groupStore.addNewUsersToGroup( + await this.groupStore.addUsersToGroup( newGroup.id, group.users, userName, @@ -215,4 +215,27 @@ export class GroupService { }); return { ...group, users: finalUsers }; } + + async syncExternalGroups( + userId: number, + externalGroups: string[], + ): Promise { + let newGroups = await this.groupStore.getNewGroupsForExternalUser( + userId, + externalGroups, + ); + await this.groupStore.addUserToGroups( + userId, + newGroups.map((g) => g.id), + ); + let oldGroups = await this.groupStore.getOldGroupsForExternalUser( + userId, + externalGroups, + ); + await this.groupStore.deleteUsersFromGroup(oldGroups); + } + + async getGroupsForUser(userId: number): Promise { + return this.groupStore.getGroupsForUser(userId); + } } diff --git a/src/lib/types/stores/group-store.ts b/src/lib/types/stores/group-store.ts index 604901833d..060f357c0e 100644 --- a/src/lib/types/stores/group-store.ts +++ b/src/lib/types/stores/group-store.ts @@ -1,5 +1,5 @@ import { Store } from './store'; -import { +import Group, { IGroup, IGroupModel, IGroupProject, @@ -15,6 +15,20 @@ export interface IStoreGroup { } export interface IGroupStore extends Store { + getGroupsForUser(userId: number): Promise; + getOldGroupsForExternalUser( + userId: number, + externalGroups: string[], + ): Promise; + addUserToGroups( + userId: number, + groupIds: number[], + createdBy?: string, + ): Promise; + getNewGroupsForExternalUser( + userId: number, + externalGroups: string[], + ): Promise; getGroupProjects(groupIds: number[]): Promise; getProjectGroupRoles(projectId: string): Promise; @@ -29,13 +43,13 @@ export interface IGroupStore extends Store { userName: string, ): Promise; - deleteOldUsersFromGroup(deletableUsers: IGroupUser[]): Promise; + deleteUsersFromGroup(deletableUsers: IGroupUser[]): Promise; update(group: IGroupModel): Promise; getAllUsersByGroups(groupIds: number[]): Promise; - addNewUsersToGroup( + addUsersToGroup( groupId: number, users: IGroupUserModel[], userName: string, diff --git a/src/test/e2e/services/access-service.e2e.test.ts b/src/test/e2e/services/access-service.e2e.test.ts index 1a21fa1842..167a793f48 100644 --- a/src/test/e2e/services/access-service.e2e.test.ts +++ b/src/test/e2e/services/access-service.e2e.test.ts @@ -881,7 +881,7 @@ test('Should be allowed move feature toggle to project when given access through description: '', }); - await groupStore.addNewUsersToGroup( + await groupStore.addUsersToGroup( groupWithProjectAccess.id, [{ user: viewerUser }], 'Admin', @@ -918,7 +918,7 @@ test('Should not lose user role access when given permissions from a group', asy description: '', }); - await groupStore.addNewUsersToGroup( + await groupStore.addUsersToGroup( groupWithNoAccess.id, [{ user: user }], 'Admin', @@ -967,13 +967,13 @@ test('Should allow user to take multiple group roles and have expected permissio description: '', }); - await groupStore.addNewUsersToGroup( + await groupStore.addUsersToGroup( groupWithCreateAccess.id, [{ user: viewerUser }], 'Admin', ); - await groupStore.addNewUsersToGroup( + await groupStore.addUsersToGroup( groupWithDeleteAccess.id, [{ user: viewerUser }], 'Admin', diff --git a/src/test/e2e/services/group-service.e2e.test.ts b/src/test/e2e/services/group-service.e2e.test.ts new file mode 100644 index 0000000000..18a3c6a128 --- /dev/null +++ b/src/test/e2e/services/group-service.e2e.test.ts @@ -0,0 +1,71 @@ +import dbInit, { ITestDb } from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; +import { createTestConfig } from '../../config/test-config'; +import { GroupService } from '../../../lib/services/group-service'; + +let stores; +let db: ITestDb; + +let groupService: GroupService; +let user; + +beforeAll(async () => { + db = await dbInit('group_service_serial', getLogger); + stores = db.stores; + user = await stores.userStore.insert({ + name: 'Some Name', + email: 'test@getunleash.io', + }); + const config = createTestConfig({ + getLogger, + }); + groupService = new GroupService(stores, config); + + await stores.groupStore.create({ + name: 'dev_group', + description: 'dev_group', + mappingsSSO: ['dev'], + }); + await stores.groupStore.create({ + name: 'maintainer_group', + description: 'maintainer_group', + mappingsSSO: ['maintainer'], + }); + + await stores.groupStore.create({ + name: 'admin_group', + description: 'admin_group', + mappingsSSO: ['admin'], + }); +}); + +afterAll(async () => { + await db.destroy(); +}); + +afterEach(async () => {}); + +test('should have three group', async () => { + const project = await groupService.getAll(); + expect(project.length).toBe(3); +}); + +test('should add person to 2 groups', async () => { + await groupService.syncExternalGroups(user.id, ['dev', 'maintainer']); + const groups = await groupService.getGroupsForUser(user.id); + expect(groups.length).toBe(2); +}); + +test('should remove person from one group', async () => { + await groupService.syncExternalGroups(user.id, ['maintainer']); + const groups = await groupService.getGroupsForUser(user.id); + expect(groups.length).toBe(1); + expect(groups[0].name).toEqual('maintainer_group'); +}); + +test('should add person to completely new group with new name', async () => { + await groupService.syncExternalGroups(user.id, ['dev']); + const groups = await groupService.getGroupsForUser(user.id); + expect(groups.length).toBe(1); + expect(groups[0].name).toEqual('dev_group'); +}); diff --git a/src/test/fixtures/fake-group-store.ts b/src/test/fixtures/fake-group-store.ts index c4511231fe..3395e6d0b6 100644 --- a/src/test/fixtures/fake-group-store.ts +++ b/src/test/fixtures/fake-group-store.ts @@ -1,5 +1,5 @@ import { IGroupStore, IStoreGroup } from '../../lib/types/stores/group-store'; -import { +import Group, { IGroup, IGroupModel, IGroupProject, @@ -42,7 +42,7 @@ export default class FakeGroupStore implements IGroupStore { throw new Error('Method not implemented.'); } - addNewUsersToGroup( + addUsersToGroup( id: number, users: IGroupUserModel[], userName: string, @@ -54,7 +54,7 @@ export default class FakeGroupStore implements IGroupStore { throw new Error('Method not implemented.'); } - deleteOldUsersFromGroup(deletableUsers: IGroupUser[]): Promise { + deleteUsersFromGroup(deletableUsers: IGroupUser[]): Promise { throw new Error('Method not implemented.'); } @@ -83,4 +83,30 @@ export default class FakeGroupStore implements IGroupStore { getGroupProjects(groupIds: number[]): Promise { throw new Error('Method not implemented.'); } + + getNewGroupsForExternalUser( + userId: number, + externalGroups: string[], + ): Promise { + throw new Error('Method not implemented.'); + } + + addUserToGroups( + userId: number, + groupIds: number[], + createdBy?: string, + ): Promise { + throw new Error('Method not implemented.'); + } + + getOldGroupsForExternalUser( + userId: number, + externalGroups: string[], + ): Promise { + throw new Error('Method not implemented.'); + } + + getGroupsForUser(userId: number): Promise { + throw new Error('Method not implemented.'); + } }