UNPKG

unleash-server

Version:

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

321 lines • 14 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const name_exists_error_1 = __importDefault(require("../error/name-exists-error")); const invalid_operation_error_1 = __importDefault(require("../error/invalid-operation-error")); const util_1 = require("../routes/util"); const project_schema_1 = require("./project-schema"); const notfound_error_1 = __importDefault(require("../error/notfound-error")); const events_1 = require("../types/events"); const model_1 = require("../types/model"); const permissions_1 = require("../types/permissions"); const no_access_error_1 = __importDefault(require("../error/no-access-error")); const incompatible_project_error_1 = __importDefault(require("../error/incompatible-project-error")); const project_1 = require("../types/project"); const project_without_owner_error_1 = __importDefault(require("../error/project-without-owner-error")); const arraysHaveSameItems_1 = require("../util/arraysHaveSameItems"); const getCreatedBy = (user) => user.email || user.username; class ProjectService { constructor({ projectStore, eventStore, featureToggleStore, featureTypeStore, environmentStore, featureEnvironmentStore, featureTagStore, userStore, }, config, accessService, featureToggleService, groupService) { this.store = projectStore; this.environmentStore = environmentStore; this.featureEnvironmentStore = featureEnvironmentStore; this.accessService = accessService; this.eventStore = eventStore; this.featureToggleStore = featureToggleStore; this.featureTypeStore = featureTypeStore; this.featureToggleService = featureToggleService; this.tagStore = featureTagStore; this.userStore = userStore; this.groupService = groupService; this.logger = config.getLogger('services/project-service.js'); } async getProjects(query, userId) { return this.store.getProjectsWithCounts(query, userId); } async getProject(id) { return this.store.get(id); } async createProject(newProject, user) { const data = await project_schema_1.projectSchema.validateAsync(newProject); await this.validateUniqueId(data.id); await this.store.create(data); const enabledEnvironments = await this.environmentStore.getAll({ enabled: true, }); // TODO: Only if enabled! await Promise.all(enabledEnvironments.map(async (e) => { await this.featureEnvironmentStore.connectProject(e.name, data.id); })); await this.accessService.createDefaultProjectRoles(user, data.id); await this.eventStore.store({ type: events_1.PROJECT_CREATED, createdBy: getCreatedBy(user), data, project: newProject.id, }); return data; } async updateProject(updatedProject, user) { const preData = await this.store.get(updatedProject.id); const project = await project_schema_1.projectSchema.validateAsync(updatedProject); await this.store.update(project); await this.eventStore.store({ type: events_1.PROJECT_UPDATED, project: project.id, createdBy: getCreatedBy(user), data: project, preData, }); } async checkProjectsCompatibility(feature, newProjectId) { const featureEnvs = await this.featureEnvironmentStore.getAll({ feature_name: feature.name, }); const newEnvs = await this.store.getEnvironmentsForProject(newProjectId); return (0, arraysHaveSameItems_1.arraysHaveSameItems)(featureEnvs.map((env) => env.environment), newEnvs); } async addEnvironmentToProject(project, environment) { await this.store.addEnvironmentToProject(project, environment); } async changeProject(newProjectId, featureName, user, currentProjectId) { const feature = await this.featureToggleStore.get(featureName); if (feature.project !== currentProjectId) { throw new no_access_error_1.default(permissions_1.MOVE_FEATURE_TOGGLE); } const project = await this.getProject(newProjectId); if (!project) { throw new notfound_error_1.default(`Project ${newProjectId} not found`); } const authorized = await this.accessService.hasPermission(user, permissions_1.MOVE_FEATURE_TOGGLE, newProjectId); if (!authorized) { throw new no_access_error_1.default(permissions_1.MOVE_FEATURE_TOGGLE); } const isCompatibleWithTargetProject = await this.checkProjectsCompatibility(feature, newProjectId); if (!isCompatibleWithTargetProject) { throw new incompatible_project_error_1.default(newProjectId); } const updatedFeature = await this.featureToggleService.changeProject(featureName, newProjectId, getCreatedBy(user)); await this.featureToggleService.updateFeatureStrategyProject(featureName, newProjectId); return updatedFeature; } async deleteProject(id, user) { if (id === project_1.DEFAULT_PROJECT) { throw new invalid_operation_error_1.default('You can not delete the default project!'); } const toggles = await this.featureToggleStore.getAll({ project: id, archived: false, }); if (toggles.length > 0) { throw new invalid_operation_error_1.default('You can not delete a project with active feature toggles'); } await this.store.delete(id); await this.eventStore.store({ type: events_1.PROJECT_DELETED, createdBy: getCreatedBy(user), project: id, }); await this.accessService.removeDefaultProjectRoles(user, id); } async validateId(id) { await util_1.nameType.validateAsync(id); await this.validateUniqueId(id); return true; } async validateUniqueId(id) { const exists = await this.store.hasProject(id); if (exists) { throw new name_exists_error_1.default('A project with this id already exists.'); } } // RBAC methods async getAccessToProject(projectId) { const [roles, users, groups] = await this.accessService.getProjectRoleAccess(projectId); return { roles, users, groups, }; } async addUser(projectId, roleId, userId, createdBy) { const [roles, users] = await this.accessService.getProjectRoleAccess(projectId); const user = await this.userStore.get(userId); const role = roles.find((r) => r.id === roleId); if (!role) { throw new notfound_error_1.default(`Could not find roleId=${roleId} on project=${projectId}`); } const alreadyHasAccess = users.some((u) => u.id === userId); if (alreadyHasAccess) { throw new Error(`User already has access to project=${projectId}`); } await this.accessService.addUserToRole(userId, role.id, projectId); await this.eventStore.store(new events_1.ProjectUserAddedEvent({ project: projectId, createdBy: createdBy || 'system-user', data: { roleId, userId, roleName: role.name, email: user.email, }, })); } async removeUser(projectId, roleId, userId, createdBy) { const role = await this.findProjectRole(projectId, roleId); await this.validateAtLeastOneOwner(projectId, role); await this.accessService.removeUserFromRole(userId, role.id, projectId); const user = await this.userStore.get(userId); await this.eventStore.store(new events_1.ProjectUserRemovedEvent({ project: projectId, createdBy, preData: { roleId, userId, roleName: role.name, email: user.email, }, })); } async addGroup(projectId, roleId, groupId, modifiedBy) { const role = await this.accessService.getRole(roleId); const group = await this.groupService.getGroup(groupId); const project = await this.getProject(projectId); await this.accessService.addGroupToRole(group.id, role.id, modifiedBy, project.id); await this.eventStore.store(new events_1.ProjectGroupAddedEvent({ project: project.id, createdBy: modifiedBy, data: { groupId: group.id, projectId: project.id, roleName: role.name, }, })); } async removeGroup(projectId, roleId, groupId, modifiedBy) { const group = await this.groupService.getGroup(groupId); const role = await this.accessService.getRole(roleId); const project = await this.getProject(projectId); await this.accessService.removeGroupFromRole(group.id, role.id, project.id); await this.eventStore.store(new events_1.ProjectGroupRemovedEvent({ project: projectId, createdBy: modifiedBy, preData: { groupId: group.id, projectId: project.id, roleName: role.name, }, })); } async addAccess(projectId, roleId, usersAndGroups, createdBy) { return this.accessService.addAccessToProject(usersAndGroups.users, usersAndGroups.groups, projectId, roleId, createdBy); } async findProjectGroupRole(projectId, roleId) { const roles = await this.groupService.getRolesForProject(projectId); const role = roles.find((r) => r.roleId === roleId); if (!role) { throw new notfound_error_1.default(`Couldn't find roleId=${roleId} on project=${projectId}`); } return role; } async findProjectRole(projectId, roleId) { const roles = await this.accessService.getRolesForProject(projectId); const role = roles.find((r) => r.id === roleId); if (!role) { throw new notfound_error_1.default(`Couldn't find roleId=${roleId} on project=${projectId}`); } return role; } async validateAtLeastOneOwner(projectId, currentRole) { if (currentRole.name === model_1.RoleName.OWNER) { const users = await this.accessService.getProjectUsersForRole(currentRole.id, projectId); const groups = await this.groupService.getProjectGroups(projectId); const roleGroups = groups.filter((g) => g.roleId == currentRole.id); if (users.length + roleGroups.length < 2) { throw new project_without_owner_error_1.default(); } } } async changeRole(projectId, roleId, userId, createdBy) { const usersWithRoles = await this.getAccessToProject(projectId); const user = usersWithRoles.users.find((u) => u.id === userId); const currentRole = usersWithRoles.roles.find((r) => r.id === user.roleId); if (currentRole.id === roleId) { // Nothing to do.... return; } await this.validateAtLeastOneOwner(projectId, currentRole); await this.accessService.updateUserProjectRole(userId, roleId, projectId); const role = await this.findProjectRole(projectId, roleId); await this.eventStore.store(new events_1.ProjectUserUpdateRoleEvent({ project: projectId, createdBy, preData: { userId, roleId: currentRole.id, roleName: currentRole.name, email: user.email, }, data: { userId, roleId, roleName: role.name, email: user.email, }, })); } async changeGroupRole(projectId, roleId, userId, createdBy) { const usersWithRoles = await this.getAccessToProject(projectId); const user = usersWithRoles.groups.find((u) => u.id === userId); const currentRole = usersWithRoles.roles.find((r) => r.id === user.roleId); if (currentRole.id === roleId) { // Nothing to do.... return; } await this.validateAtLeastOneOwner(projectId, currentRole); await this.accessService.updateGroupProjectRole(userId, roleId, projectId); const role = await this.findProjectGroupRole(projectId, roleId); await this.eventStore.store(new events_1.ProjectGroupUpdateRoleEvent({ project: projectId, createdBy, preData: { userId, roleId: currentRole.id, roleName: currentRole.name, }, data: { userId, roleId, roleName: role.name, }, })); } async getMembers(projectId) { return this.store.getMembersCountByProject(projectId); } async getProjectsByUser(userId) { return this.store.getProjectsByUser(userId); } async getProjectOverview(projectId, archived = false) { const project = await this.store.get(projectId); const environments = await this.store.getEnvironmentsForProject(projectId); const features = await this.featureToggleService.getFeatureOverview({ projectId, archived, }); const members = await this.store.getMembersCountByProject(projectId); return { name: project.name, environments, description: project.description, health: project.health, features, members, version: 1, }; } } exports.default = ProjectService; //# sourceMappingURL=project-service.js.map