mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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<void> {
 | 
			
		||||
@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<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 {
 | 
			
		||||
import Group, {
 | 
			
		||||
    IGroup,
 | 
			
		||||
    IGroupModel,
 | 
			
		||||
    IGroupProject,
 | 
			
		||||
@ -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[]>;
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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 {
 | 
			
		||||
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<void> {
 | 
			
		||||
    deleteUsersFromGroup(deletableUsers: IGroupUser[]): Promise<void> {
 | 
			
		||||
        throw new Error('Method not implemented.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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.');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user