UNPKG

unleash-server

Version:

Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.

193 lines • 8.72 kB
import { GroupDeletedEvent, GroupUpdatedEvent, SYSTEM_USER_AUDIT, } from '../types/index.js'; import BadDataError from '../error/bad-data-error.js'; import { GROUP_CREATED } from '../events/index.js'; import { GroupUserAdded, GroupUserRemoved, ScimGroupsDeleted, } from '../types/index.js'; import NameExistsError from '../error/name-exists-error.js'; import { SSO_SYNC_USER } from '../db/group-store.js'; import { NotFoundError } from '../error/index.js'; const setsAreEqual = (firstSet, secondSet) => firstSet.size === secondSet.size && [...firstSet].every((x) => secondSet.has(x)); export class GroupService { constructor(stores, { getLogger }, eventService) { this.logger = getLogger('service/group-service.js'); this.groupStore = stores.groupStore; this.eventService = eventService; this.accountStore = stores.accountStore; } async getAll() { const groups = await this.groupStore.getAll(); const allGroupUsers = await this.groupStore.getAllUsersByGroups(groups.map((g) => g.id)); const users = await this.accountStore.getAllWithId(allGroupUsers.map((u) => u.userId)); const groupProjects = await this.groupStore.getGroupProjects(groups.map((g) => g.id)); return groups.map((group) => { const mappedGroup = this.mapGroupWithUsers(group, allGroupUsers, users); return this.mapGroupWithProjects(groupProjects, mappedGroup); }); } async getAllWithId(ids) { return this.groupStore.getAllWithId(ids); } mapGroupWithProjects(groupProjects, group) { return { ...group, projects: groupProjects .filter((project) => project.groupId === group.id) .map((project) => project.project), }; } async getGroup(id) { const group = await this.groupStore.get(id); if (group === undefined) { throw new NotFoundError(`Could not find group with id ${id}`); } const groupUsers = await this.groupStore.getAllUsersByGroups([id]); const users = await this.accountStore.getAllWithId(groupUsers.map((u) => u.userId)); return this.mapGroupWithUsers(group, groupUsers, users); } async isScimGroup(id) { const group = await this.groupStore.get(id); return Boolean(group?.scimId); } async createGroup(group, auditUser) { await this.validateGroup(group); const newGroup = await this.groupStore.create(group); if (group.users) { await this.groupStore.addUsersToGroup(newGroup.id, group.users, auditUser.username); } const newUserIds = group.users?.map((g) => g.user.id); await this.eventService.storeEvent({ type: GROUP_CREATED, createdBy: auditUser.username, createdByUserId: auditUser.id, ip: auditUser.ip, data: { ...group, users: newUserIds }, }); return newGroup; } async updateGroup(group, auditUser) { const existingGroup = await this.groupStore.get(group.id); await this.validateGroup(group, existingGroup); const newGroup = await this.groupStore.update(group); const existingUsers = await this.groupStore.getAllUsersByGroups([ group.id, ]); const existingUserIds = existingUsers.map((g) => g.userId); const deletableUsers = existingUsers.filter((existingUser) => !group.users.some((groupUser) => groupUser.user.id === existingUser.userId)); await this.groupStore.updateGroupUsers(newGroup.id, group.users.filter((user) => !existingUserIds.includes(user.user.id)), deletableUsers, auditUser.username); const newUserIds = group.users.map((g) => g.user.id); await this.eventService.storeEvent(new GroupUpdatedEvent({ data: { ...newGroup, users: newUserIds }, preData: { ...existingGroup, users: existingUserIds }, auditUser, })); return newGroup; } async getProjectGroups(projectId) { const projectGroups = await this.groupStore.getProjectGroups(projectId); if (projectGroups.length > 0) { const groups = await this.groupStore.getAllWithId(projectGroups.map((g) => g.id)); const groupUsers = await this.groupStore.getAllUsersByGroups(groups.map((g) => g.id)); const users = await this.accountStore.getAllWithId(groupUsers.map((u) => u.userId)); return groups.flatMap((group) => { return projectGroups .filter((gr) => gr.id === group.id) .map((groupRole) => ({ ...this.mapGroupWithUsers(group, groupUsers, users), ...groupRole, })); }); } return []; } async deleteGroup(id, auditUser) { const group = await this.groupStore.get(id); if (group === undefined) { /// Group was already deleted, or never existed, do nothing return; } const existingUsers = await this.groupStore.getAllUsersByGroups([ group.id, ]); const existingUserIds = existingUsers.map((g) => g.userId); await this.groupStore.delete(id); await this.eventService.storeEvent(new GroupDeletedEvent({ preData: { ...group, users: existingUserIds }, auditUser, })); } async validateGroup(group, existingGroup) { if (!group.name) { throw new BadDataError('Group name cannot be empty'); } if (!existingGroup || existingGroup.name !== group.name) { if (await this.groupStore.existsWithName(group.name)) { throw new NameExistsError('Group name already exists'); } } if (existingGroup && Boolean(existingGroup.scimId)) { if (existingGroup.name !== group.name) { throw new BadDataError('Cannot update the name of a SCIM group'); } const existingUsers = new Set((await this.groupStore.getAllUsersByGroups([ existingGroup.id, ])).map((g) => g.userId)); const newUsers = new Set(group.users?.map((g) => g.user.id) || []); if (!setsAreEqual(existingUsers, newUsers)) { throw new BadDataError('Cannot update users of a SCIM group'); } } } async getRolesForProject(projectId) { return this.groupStore.getProjectGroupRoles(projectId); } async syncExternalGroups(userId, externalGroups, createdBy, // deprecated createdByUserId) { if (Array.isArray(externalGroups)) { const newGroups = await this.groupStore.getNewGroupsForExternalUser(userId, externalGroups); await this.groupStore.addUserToGroups(userId, newGroups.map((g) => g.id), SSO_SYNC_USER); const oldGroups = await this.groupStore.getOldGroupsForExternalUser(userId, externalGroups); await this.groupStore.deleteUsersFromGroup(oldGroups); const events = []; for (const group of newGroups) { events.push(new GroupUserAdded({ userId, groupId: group.id, auditUser: SYSTEM_USER_AUDIT, })); } for (const group of oldGroups) { events.push(new GroupUserRemoved({ userId, groupId: group.groupId, auditUser: SYSTEM_USER_AUDIT, })); } await this.eventService.storeEvents(events); } } async deleteScimGroups(auditUser) { await this.groupStore.deleteScimGroups(); await this.eventService.storeEvent(new ScimGroupsDeleted({ data: null, auditUser, })); } mapGroupWithUsers(group, allGroupUsers, allUsers) { const groupUsers = allGroupUsers.filter((user) => user.groupId === group.id); const groupUsersId = groupUsers.map((user) => user.userId); const selectedUsers = allUsers.filter((user) => groupUsersId.includes(user.id)); const finalUsers = selectedUsers.map((user) => { const roleUser = groupUsers.find((gu) => gu.userId === user.id); return { user: user, joinedAt: roleUser?.joinedAt, createdBy: roleUser?.createdBy, }; }); return { ...group, users: finalUsers }; } async getGroupsForUser(userId) { return this.groupStore.getGroupsForUser(userId); } } //# sourceMappingURL=group-service.js.map