UNPKG

unleash-server

Version:

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

514 lines • 21.5 kB
import * as permissions from '../types/permissions.js'; import { RoleName, } from '../types/model.js'; import NameExistsError from '../error/name-exists-error.js'; import RoleInUseError from '../error/role-in-use-error.js'; import { roleSchema } from '../schema/role-schema.js'; import { ALL_ENVS, ALL_PROJECTS, CUSTOM_PROJECT_ROLE_TYPE, CUSTOM_ROOT_ROLE_TYPE, ROOT_ROLE_TYPES, } from '../util/constants.js'; import { DEFAULT_PROJECT } from '../types/project.js'; import InvalidOperationError from '../error/invalid-operation-error.js'; import BadDataError from '../error/bad-data-error.js'; import { RoleCreatedEvent, RoleDeletedEvent, RoleUpdatedEvent, } from '../types/index.js'; import { NotFoundError } from '../error/index.js'; const { ADMIN } = permissions; const PROJECT_ADMIN = [ permissions.UPDATE_PROJECT, permissions.DELETE_PROJECT, permissions.CREATE_FEATURE, permissions.UPDATE_FEATURE, permissions.DELETE_FEATURE, ]; const isProjectPermission = (permission) => PROJECT_ADMIN.includes(permission); export const cleanPermissionEnvironment = (permissions) => { return permissions?.map((permission) => { if (permission.environment === '') { return { ...permission, environment: null }; } return permission; }); }; export class AccessService { constructor({ accessStore, accountStore, roleStore, environmentStore, }, // TODO remove groupStore later, kept for backward compatibility with enterprise { getLogger }, groupService, eventService) { this.store = accessStore; this.accountStore = accountStore; this.roleStore = roleStore; this.groupService = groupService; this.environmentStore = environmentStore; this.logger = getLogger('/services/access-service.ts'); this.eventService = eventService; } meetsAllPermissions(userP, permissionsArray, projectId, environment) { return userP .filter((p) => !p.project || p.project === projectId || p.project === ALL_PROJECTS) .filter((p) => !p.environment || p.environment === environment || p.environment === ALL_ENVS) .some((p) => permissionsArray.includes(p.permission) || p.permission === ADMIN); } /** * Used to check if a user has access to the requested resource * * @param user * @param permission * @param projectId */ async hasPermission(user, permission, projectId, environment) { const permissionsArray = Array.isArray(permission) ? permission : [permission]; const permissionLogInfo = permissionsArray.length === 1 ? `permission=${permissionsArray[0]}` : `permissions=[${permissionsArray.join(',')}]`; this.logger.info(`Checking ${permissionLogInfo}, userId=${user.id}, projectId=${projectId}, environment=${environment}`); try { const userP = await this.getPermissionsForUser(user); return this.meetsAllPermissions(userP, permissionsArray, projectId, environment); } catch (e) { this.logger.error(`Error checking ${permissionLogInfo}, userId=${user.id} projectId=${projectId}`, e); return Promise.resolve(false); } } /** * Returns all roles the user has in the project. * Including roles via groups. * In addition, it includes root roles * @param userId user to find roles for * @param project project to find roles for */ async getAllProjectRolesForUser(userId, project) { return this.store.getAllProjectRolesForUser(userId, project); } /** * Check a user against all available permissions. * Provided a project, project permissions will be checked against that project. * Provided an environment, environment permissions will be checked against that environment (and project). */ async getAccessOverviewForUser(user, projectId, environment) { const permissions = await this.getPermissions(); const userP = await this.getPermissionsForUser(user); const overview = { root: permissions.root.map((p) => ({ ...p, hasPermission: this.meetsAllPermissions(userP, [p.name]), })), project: permissions.project.map((p) => ({ ...p, hasPermission: this.meetsAllPermissions(userP, [p.name], projectId), })), environment: permissions.environments .find((ep) => ep.name === environment) ?.permissions.map((p) => ({ ...p, hasPermission: this.meetsAllPermissions(userP, [p.name], projectId, environment), })) ?? [], }; return overview; } async getPermissionsForUser(user) { if (user.isAPI) { return user.permissions?.map((p) => ({ permission: p, })); } return this.store.getPermissionsForUser(user.id); } async getPermissions() { const bindablePermissions = await this.store.getAvailablePermissions(); const environments = await this.environmentStore.getAll(); const rootPermissions = bindablePermissions.filter(({ type }) => type === 'root'); const projectPermissions = bindablePermissions.filter((x) => { return x.type === 'project'; }); const environmentPermissions = bindablePermissions.filter((perm) => { return perm.type === 'environment'; }); const allEnvironmentPermissions = environments.map((env) => { return { name: env.name, permissions: environmentPermissions.map((permission) => { return { environment: env.name, ...permission }; }), }; }); return { root: rootPermissions, project: projectPermissions, environments: allEnvironmentPermissions, }; } async addUserToRole(userId, roleId, projectId) { return this.store.addUserToRole(userId, roleId, projectId); } async addGroupToRole(groupId, roleId, createdBy, projectId) { return this.store.addGroupToRole(groupId, roleId, createdBy, projectId); } async addAccessToProject(roles, groups, users, projectId, createdBy) { if (roles.length === 0) { throw new BadDataError("You can't grant access without any roles. The roles array you sent was empty."); } return this.store.addAccessToProject(roles, groups, users, projectId, createdBy); } async setProjectRolesForUser(projectId, userId, roles) { await this.store.setProjectRolesForUser(projectId, userId, roles); } async getProjectRolesForUser(projectId, userId) { return this.store.getProjectRolesForUser(projectId, userId); } async setProjectRolesForGroup(projectId, groupId, roles, createdBy) { await this.store.setProjectRolesForGroup(projectId, groupId, roles, createdBy); } async getProjectRolesForGroup(projectId, groupId) { return this.store.getProjectRolesForGroup(projectId, groupId); } async getRoleByName(roleName) { return this.roleStore.getRoleByName(roleName); } async removeUserAccess(projectId, userId) { await this.store.removeUserAccess(projectId, userId); } async removeGroupAccess(projectId, groupId) { await this.store.removeGroupAccess(projectId, groupId); } async setUserRootRole(userId, role) { const newRootRole = await this.resolveRootRole(role); if (newRootRole) { try { await this.store.removeRolesOfTypeForUser(userId, ROOT_ROLE_TYPES); await this.store.addUserToRole(userId, newRootRole.id, DEFAULT_PROJECT); } catch (error) { const message = `Could not add role=${newRootRole.name} to userId=${userId}`; this.logger.error(message, error); throw new Error(message); } } else { throw new BadDataError(`Could not find rootRole=${role}`); } } async getRootRoleForUser(userId) { const rootRole = await this.store.getRootRoleForUser(userId); if (!rootRole) { // this should never happen, but before breaking we want to know if it does. this.logger.warn(`Could not find root role for user=${userId}.`); return this.getPredefinedRole(RoleName.VIEWER); } return rootRole; } async removeUserFromRole(userId, roleId, projectId) { return this.store.removeUserFromRole(userId, roleId, projectId); } async updateUserProjectRole(userId, roleId, projectId) { return this.store.updateUserProjectRole(userId, roleId, projectId); } //This actually only exists for testing purposes async addPermissionToRole(roleId, permission, environment) { if (isProjectPermission(permission) && !environment) { throw new Error(`ProjectId cannot be empty for permission=${permission}`); } return this.store.addPermissionsToRole(roleId, [{ name: permission }], environment); } //This actually only exists for testing purposes async removePermissionFromRole(roleId, permission, environment) { if (isProjectPermission(permission) && !environment) { throw new Error(`ProjectId cannot be empty for permission=${permission}`); } return this.store.removePermissionFromRole(roleId, permission, environment); } async getRoles() { return this.roleStore.getRoles(); } async getRole(id) { const role = await this.store.get(id); if (role === undefined) { throw new NotFoundError(`Could not find role with id ${id}`); } const rolePermissions = await this.store.getPermissionsForRole(role.id); return { ...role, permissions: rolePermissions, }; } async getRoleData(roleId) { const [role, rolePerms, users] = await Promise.all([ this.store.get(roleId), this.store.getPermissionsForRole(roleId), this.getUsersForRole(roleId), ]); if (role === undefined) { throw new NotFoundError(`Could not find role with id ${roleId}`); } return { role, permissions: rolePerms, users }; } async getProjectRoles() { return this.roleStore.getProjectRoles(); } async getRolesForProject(projectId) { return this.roleStore.getRolesForProject(projectId); } async getRolesForUser(userId) { return this.store.getRolesForUserId(userId); } async wipeUserPermissions(userId) { return Promise.all([ this.store.unlinkUserRoles(userId), this.store.unlinkUserGroups(userId), this.store.clearUserPersonalAccessTokens(userId), this.store.clearPublicSignupUserTokens(userId), ]); } async getUsersForRole(roleId) { const userIdList = await this.store.getUserIdsForRole(roleId); if (userIdList.length > 0) { return this.accountStore.getAllWithId(userIdList); } return []; } async getGroupsForRole(roleId) { const groupdIdList = await this.store.getGroupIdsForRole(roleId); if (groupdIdList.length > 0) { return this.groupService.getAllWithId(groupdIdList); } return []; } async getProjectUsersForRole(roleId, projectId) { const userRoleList = await this.store.getProjectUsersForRole(roleId, projectId); if (userRoleList.length > 0) { const userIdList = userRoleList.map((u) => u.userId); const users = await this.accountStore.getAllWithId(userIdList); return users.map((user) => { const role = userRoleList.find((r) => r.userId === user.id); return { ...user, addedAt: role.addedAt, roleId, }; }); } return []; } async getProjectUsers(projectId) { const projectUsers = await this.store.getProjectUsers(projectId); if (projectUsers.length > 0) { const users = await this.accountStore.getAllWithId(projectUsers.map((u) => u.id)); return users.flatMap((user) => { return projectUsers .filter((u) => u.id === user.id) .map((groupUser) => ({ ...user, ...groupUser, })); }); } return []; } async getProjectRoleAccess(projectId) { const roles = await this.roleStore.getProjectRoles(); const users = await this.getProjectUsers(projectId); const groups = await this.groupService.getProjectGroups(projectId); return { roles, groups, users, }; } async getProjectRoleUsage(roleId) { return this.store.getProjectUserAndGroupCountsForRole(roleId); } async createDefaultProjectRoles(owner, projectId) { if (!projectId) { throw new Error('ProjectId cannot be empty'); } const ownerRole = await this.roleStore.getRoleByName(RoleName.OWNER); // TODO: remove this when all users is guaranteed to have a unique id. if (owner.id) { this.logger.info(`Making ${owner.id} admin of ${projectId} via roleId=${ownerRole.id}`); await this.store.addUserToRole(owner.id, ownerRole.id, projectId); } } async removeDefaultProjectRoles(_owner, projectId) { this.logger.info(`Removing project roles for ${projectId}`); return this.roleStore.removeRolesForProject(projectId); } async getRootRoleForAllUsers() { return this.roleStore.getRootRoleForAllUsers(); } async getRootRoles() { return this.roleStore.getRootRoles(); } async resolveRootRole(rootRole) { const rootRoles = await this.getRootRoles(); let role; if (typeof rootRole === 'number') { role = rootRoles.find((r) => r.id === rootRole); } else { role = rootRoles.find((r) => r.name === rootRole); } return role; } /* This method is intended to give a predicable way to fetch predefined roles defined in the RoleName enum. This method should not be used to fetch custom root or project roles. */ async getPredefinedRole(roleName) { const roles = await this.roleStore.getRoles(); const role = roles.find((r) => r.name === roleName); if (!role) { throw new BadDataError(`Could not find predefined role with name ${RoleName}`); } return role; } async getAllRoles() { return this.roleStore.getAll(); } async createRole(role, auditUser) { // CUSTOM_PROJECT_ROLE_TYPE is assumed by default for backward compatibility const roleType = role.type === CUSTOM_ROOT_ROLE_TYPE ? CUSTOM_ROOT_ROLE_TYPE : CUSTOM_PROJECT_ROLE_TYPE; const baseRole = { ...(await this.validateRole(role)), roleType, }; await this.validatePermissions(role.permissions); const rolePermissions = cleanPermissionEnvironment(role.permissions); const newRole = await this.roleStore.create(baseRole); if (rolePermissions) { if (roleType === CUSTOM_ROOT_ROLE_TYPE) { // this branch uses named permissions await this.store.addPermissionsToRole(newRole.id, rolePermissions); } else { // this branch uses id permissions await this.store.addEnvironmentPermissionsToRole(newRole.id, rolePermissions); } } const addedPermissions = await this.store.getPermissionsForRole(newRole.id); await this.eventService.storeEvent(new RoleCreatedEvent({ data: { ...newRole, permissions: this.sanitizePermissions(addedPermissions), }, auditUser, })); return newRole; } async updateRole(role, auditUser) { const roleType = role.type === CUSTOM_ROOT_ROLE_TYPE ? CUSTOM_ROOT_ROLE_TYPE : CUSTOM_PROJECT_ROLE_TYPE; await this.validateRole(role, role.id); const existingRole = await this.roleStore.get(role.id); const baseRole = { id: role.id, name: role.name, description: role.description, roleType, }; await this.validatePermissions(role.permissions); const rolePermissions = cleanPermissionEnvironment(role.permissions); const updatedRole = await this.roleStore.update(baseRole); const existingPermissions = await this.store.getPermissionsForRole(role.id); if (rolePermissions) { await this.store.wipePermissionsFromRole(updatedRole.id); if (roleType === CUSTOM_ROOT_ROLE_TYPE) { await this.store.addPermissionsToRole(updatedRole.id, rolePermissions); } else { await this.store.addEnvironmentPermissionsToRole(updatedRole.id, rolePermissions); } } const updatedPermissions = await this.store.getPermissionsForRole(role.id); await this.eventService.storeEvent(new RoleUpdatedEvent({ data: { ...updatedRole, permissions: this.sanitizePermissions(updatedPermissions), }, preData: { ...existingRole, permissions: this.sanitizePermissions(existingPermissions), }, auditUser, })); return updatedRole; } sanitizePermissions(permissions) { return permissions.map(({ name, environment }) => { const sanitizedEnvironment = environment && environment !== null && environment !== '' ? environment : undefined; return { name, environment: sanitizedEnvironment }; }); } async deleteRole(id, deletedBy) { await this.validateRoleIsNotBuiltIn(id); const roleUsers = await this.getUsersForRole(id); const roleGroups = await this.getGroupsForRole(id); if (roleUsers.length > 0 || roleGroups.length > 0) { throw new RoleInUseError(`Role is in use by users(${roleUsers.length}) or groups(${roleGroups.length}). You cannot delete a role that is in use without first removing the role from the users and groups.`); } const existingRole = await this.roleStore.get(id); const existingPermissions = await this.store.getPermissionsForRole(id); await this.roleStore.delete(id); await this.eventService.storeEvent(new RoleDeletedEvent({ preData: { ...existingRole, permissions: this.sanitizePermissions(existingPermissions), }, auditUser: deletedBy, })); return; } async validateRoleIsUnique(roleName, existingId) { const exists = await this.roleStore.nameInUse(roleName, existingId); if (exists) { throw new NameExistsError(`There already exists a role with the name ${roleName}`); } return Promise.resolve(); } async validateRoleIsNotBuiltIn(roleId) { const role = await this.store.get(roleId); if (role === undefined) { throw new InvalidOperationError('You cannot change a non-existing role'); } if (role.type !== CUSTOM_PROJECT_ROLE_TYPE && role.type !== CUSTOM_ROOT_ROLE_TYPE) { throw new InvalidOperationError('You cannot change built in roles.'); } } async validateRole(role, existingId) { const cleanedRole = await roleSchema.validateAsync(role); if (existingId) { await this.validateRoleIsNotBuiltIn(existingId); } await this.validateRoleIsUnique(role.name, existingId); return cleanedRole; } async getUserAccessOverview() { return this.store.getUserAccessOverview(); } async validatePermissions(permissions) { if (!permissions?.length) { return; } const availablePermissions = await this.store.getAvailablePermissions(); const invalidPermissions = permissions.filter((permission) => !availablePermissions.some((availablePermission) => 'id' in permission ? availablePermission.id === permission.id : availablePermission.name === permission.name)); if (invalidPermissions.length > 0) { const invalidPermissionList = invalidPermissions .map((permission) => 'id' in permission ? `permission with ID: ${permission.id}` : permission.name) .join(', '); throw new BadDataError(`Invalid permissions supplied. The following permissions don't exist: ${invalidPermissionList}.`); } } } //# sourceMappingURL=access-service.js.map