mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-17 01:17:29 +02:00
Syncing external groups with unleash group (#2194)
* Syncing groups * Add tests
This commit is contained in:
parent
e153eab2d1
commit
06ebe4fca0
@ -169,7 +169,7 @@ export default class GroupStore implements IGroupStore {
|
|||||||
return rowToGroup(row[0]);
|
return rowToGroup(row[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addNewUsersToGroup(
|
async addUsersToGroup(
|
||||||
groupId: number,
|
groupId: number,
|
||||||
users: IGroupUserModel[],
|
users: IGroupUserModel[],
|
||||||
userName: string,
|
userName: string,
|
||||||
@ -185,7 +185,7 @@ export default class GroupStore implements IGroupStore {
|
|||||||
return (transaction || this.db).batchInsert(T.GROUP_USER, rows);
|
return (transaction || this.db).batchInsert(T.GROUP_USER, rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteOldUsersFromGroup(
|
async deleteUsersFromGroup(
|
||||||
deletableUsers: IGroupUser[],
|
deletableUsers: IGroupUser[],
|
||||||
transaction?: Transaction,
|
transaction?: Transaction,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -205,8 +205,65 @@ export default class GroupStore implements IGroupStore {
|
|||||||
userName: string,
|
userName: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.db.transaction(async (tx) => {
|
await this.db.transaction(async (tx) => {
|
||||||
await this.addNewUsersToGroup(groupId, newUsers, userName, tx);
|
await this.addUsersToGroup(groupId, newUsers, userName, tx);
|
||||||
await this.deleteOldUsersFromGroup(deletableUsers, 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@ export class GroupService {
|
|||||||
|
|
||||||
const newGroup = await this.groupStore.create(group);
|
const newGroup = await this.groupStore.create(group);
|
||||||
|
|
||||||
await this.groupStore.addNewUsersToGroup(
|
await this.groupStore.addUsersToGroup(
|
||||||
newGroup.id,
|
newGroup.id,
|
||||||
group.users,
|
group.users,
|
||||||
userName,
|
userName,
|
||||||
@ -215,4 +215,27 @@ export class GroupService {
|
|||||||
});
|
});
|
||||||
return { ...group, users: finalUsers };
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Store } from './store';
|
import { Store } from './store';
|
||||||
import {
|
import Group, {
|
||||||
IGroup,
|
IGroup,
|
||||||
IGroupModel,
|
IGroupModel,
|
||||||
IGroupProject,
|
IGroupProject,
|
||||||
@ -15,6 +15,20 @@ export interface IStoreGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IGroupStore extends Store<IGroup, number> {
|
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[]>;
|
getGroupProjects(groupIds: number[]): Promise<IGroupProject[]>;
|
||||||
|
|
||||||
getProjectGroupRoles(projectId: string): Promise<IGroupRole[]>;
|
getProjectGroupRoles(projectId: string): Promise<IGroupRole[]>;
|
||||||
@ -29,13 +43,13 @@ export interface IGroupStore extends Store<IGroup, number> {
|
|||||||
userName: string,
|
userName: string,
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
deleteOldUsersFromGroup(deletableUsers: IGroupUser[]): Promise<void>;
|
deleteUsersFromGroup(deletableUsers: IGroupUser[]): Promise<void>;
|
||||||
|
|
||||||
update(group: IGroupModel): Promise<IGroup>;
|
update(group: IGroupModel): Promise<IGroup>;
|
||||||
|
|
||||||
getAllUsersByGroups(groupIds: number[]): Promise<IGroupUser[]>;
|
getAllUsersByGroups(groupIds: number[]): Promise<IGroupUser[]>;
|
||||||
|
|
||||||
addNewUsersToGroup(
|
addUsersToGroup(
|
||||||
groupId: number,
|
groupId: number,
|
||||||
users: IGroupUserModel[],
|
users: IGroupUserModel[],
|
||||||
userName: string,
|
userName: string,
|
||||||
|
@ -881,7 +881,7 @@ test('Should be allowed move feature toggle to project when given access through
|
|||||||
description: '',
|
description: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
await groupStore.addNewUsersToGroup(
|
await groupStore.addUsersToGroup(
|
||||||
groupWithProjectAccess.id,
|
groupWithProjectAccess.id,
|
||||||
[{ user: viewerUser }],
|
[{ user: viewerUser }],
|
||||||
'Admin',
|
'Admin',
|
||||||
@ -918,7 +918,7 @@ test('Should not lose user role access when given permissions from a group', asy
|
|||||||
description: '',
|
description: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
await groupStore.addNewUsersToGroup(
|
await groupStore.addUsersToGroup(
|
||||||
groupWithNoAccess.id,
|
groupWithNoAccess.id,
|
||||||
[{ user: user }],
|
[{ user: user }],
|
||||||
'Admin',
|
'Admin',
|
||||||
@ -967,13 +967,13 @@ test('Should allow user to take multiple group roles and have expected permissio
|
|||||||
description: '',
|
description: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
await groupStore.addNewUsersToGroup(
|
await groupStore.addUsersToGroup(
|
||||||
groupWithCreateAccess.id,
|
groupWithCreateAccess.id,
|
||||||
[{ user: viewerUser }],
|
[{ user: viewerUser }],
|
||||||
'Admin',
|
'Admin',
|
||||||
);
|
);
|
||||||
|
|
||||||
await groupStore.addNewUsersToGroup(
|
await groupStore.addUsersToGroup(
|
||||||
groupWithDeleteAccess.id,
|
groupWithDeleteAccess.id,
|
||||||
[{ user: viewerUser }],
|
[{ user: viewerUser }],
|
||||||
'Admin',
|
'Admin',
|
||||||
|
71
src/test/e2e/services/group-service.e2e.test.ts
Normal file
71
src/test/e2e/services/group-service.e2e.test.ts
Normal file
@ -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
src/test/fixtures/fake-group-store.ts
vendored
32
src/test/fixtures/fake-group-store.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
import { IGroupStore, IStoreGroup } from '../../lib/types/stores/group-store';
|
import { IGroupStore, IStoreGroup } from '../../lib/types/stores/group-store';
|
||||||
import {
|
import Group, {
|
||||||
IGroup,
|
IGroup,
|
||||||
IGroupModel,
|
IGroupModel,
|
||||||
IGroupProject,
|
IGroupProject,
|
||||||
@ -42,7 +42,7 @@ export default class FakeGroupStore implements IGroupStore {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
addNewUsersToGroup(
|
addUsersToGroup(
|
||||||
id: number,
|
id: number,
|
||||||
users: IGroupUserModel[],
|
users: IGroupUserModel[],
|
||||||
userName: string,
|
userName: string,
|
||||||
@ -54,7 +54,7 @@ export default class FakeGroupStore implements IGroupStore {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteOldUsersFromGroup(deletableUsers: IGroupUser[]): Promise<void> {
|
deleteUsersFromGroup(deletableUsers: IGroupUser[]): Promise<void> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,4 +83,30 @@ export default class FakeGroupStore implements IGroupStore {
|
|||||||
getGroupProjects(groupIds: number[]): Promise<IGroupProject[]> {
|
getGroupProjects(groupIds: number[]): Promise<IGroupProject[]> {
|
||||||
throw new Error('Method not implemented.');
|
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.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user