Skip to content

Commit

Permalink
Syncing external groups with unleash group (#2194)
Browse files Browse the repository at this point in the history
* Syncing groups

* Add tests
  • Loading branch information
sjaanus committed Oct 14, 2022
1 parent e153eab commit 06ebe4f
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 15 deletions.
65 changes: 61 additions & 4 deletions src/lib/db/group-store.ts
Expand Up @@ -169,7 +169,7 @@ export default class GroupStore implements IGroupStore {
return rowToGroup(row[0]);
}

async addNewUsersToGroup(
async addUsersToGroup(
groupId: number,
users: IGroupUserModel[],
userName: string,
Expand All @@ -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<void> {
Expand All @@ -205,8 +205,65 @@ export default class GroupStore implements IGroupStore {
userName: string,
): Promise<void> {
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<IGroup[]> {
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<void> {
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<IGroupUser[]> {
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<Group[]> {
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);
}
}
25 changes: 24 additions & 1 deletion src/lib/services/group-service.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -215,4 +215,27 @@ export class GroupService {
});
return { ...group, users: finalUsers };
}

async syncExternalGroups(
userId: number,
externalGroups: string[],
): Promise<void> {
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<IGroup[]> {
return this.groupStore.getGroupsForUser(userId);
}
}
20 changes: 17 additions & 3 deletions src/lib/types/stores/group-store.ts
@@ -1,5 +1,5 @@
import { Store } from './store';
import {
import Group, {
IGroup,
IGroupModel,
IGroupProject,
Expand All @@ -15,6 +15,20 @@ export interface IStoreGroup {
}

export interface IGroupStore extends Store<IGroup, number> {
getGroupsForUser(userId: number): Promise<Group[]>;
getOldGroupsForExternalUser(
userId: number,
externalGroups: string[],
): Promise<IGroupUser[]>;
addUserToGroups(
userId: number,
groupIds: number[],
createdBy?: string,
): Promise<void>;
getNewGroupsForExternalUser(
userId: number,
externalGroups: string[],
): Promise<IGroup[]>;
getGroupProjects(groupIds: number[]): Promise<IGroupProject[]>;

getProjectGroupRoles(projectId: string): Promise<IGroupRole[]>;
Expand All @@ -29,13 +43,13 @@ export interface IGroupStore extends Store<IGroup, number> {
userName: string,
): Promise<void>;

deleteOldUsersFromGroup(deletableUsers: IGroupUser[]): Promise<void>;
deleteUsersFromGroup(deletableUsers: IGroupUser[]): Promise<void>;

update(group: IGroupModel): Promise<IGroup>;

getAllUsersByGroups(groupIds: number[]): Promise<IGroupUser[]>;

addNewUsersToGroup(
addUsersToGroup(
groupId: number,
users: IGroupUserModel[],
userName: string,
Expand Down
8 changes: 4 additions & 4 deletions src/test/e2e/services/access-service.e2e.test.ts
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
71 changes: 71 additions & 0 deletions 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');
});
32 changes: 29 additions & 3 deletions 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,
Expand Down Expand Up @@ -42,7 +42,7 @@ export default class FakeGroupStore implements IGroupStore {
throw new Error('Method not implemented.');
}

addNewUsersToGroup(
addUsersToGroup(
id: number,
users: IGroupUserModel[],
userName: string,
Expand All @@ -54,7 +54,7 @@ export default class FakeGroupStore implements IGroupStore {
throw new Error('Method not implemented.');
}

deleteOldUsersFromGroup(deletableUsers: IGroupUser[]): Promise<void> {
deleteUsersFromGroup(deletableUsers: IGroupUser[]): Promise<void> {
throw new Error('Method not implemented.');
}

Expand Down Expand Up @@ -83,4 +83,30 @@ export default class FakeGroupStore implements IGroupStore {
getGroupProjects(groupIds: number[]): Promise<IGroupProject[]> {
throw new Error('Method not implemented.');
}

getNewGroupsForExternalUser(
userId: number,
externalGroups: string[],
): Promise<IGroup[]> {
throw new Error('Method not implemented.');
}

addUserToGroups(
userId: number,
groupIds: number[],
createdBy?: string,
): Promise<void> {
throw new Error('Method not implemented.');
}

getOldGroupsForExternalUser(
userId: number,
externalGroups: string[],
): Promise<IGroupUser[]> {
throw new Error('Method not implemented.');
}

getGroupsForUser(userId: number): Promise<Group[]> {
throw new Error('Method not implemented.');
}
}

0 comments on commit 06ebe4f

Please sign in to comment.