UNPKG

unleash-server

Version:

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

308 lines • 12.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const bcryptjs_1 = __importDefault(require("bcryptjs")); const owasp_password_strength_test_1 = __importDefault(require("owasp-password-strength-test")); const joi_1 = __importDefault(require("joi")); const user_1 = __importDefault(require("../types/user")); const is_email_1 = __importDefault(require("../util/is-email")); const invalid_token_error_1 = __importDefault(require("../error/invalid-token-error")); const notfound_error_1 = __importDefault(require("../error/notfound-error")); const owasp_validation_error_1 = __importDefault(require("../error/owasp-validation-error")); const password_undefined_1 = __importDefault(require("../error/password-undefined")); const events_1 = require("../types/events"); const model_1 = require("../types/model"); const simple_auth_settings_1 = require("../types/settings/simple-auth-settings"); const disabled_error_1 = __importDefault(require("../error/disabled-error")); const password_mismatch_1 = __importDefault(require("../error/password-mismatch")); const bad_data_error_1 = __importDefault(require("../error/bad-data-error")); const isDefined_1 = require("../util/isDefined"); const date_fns_1 = require("date-fns"); const systemUser = new user_1.default({ id: -1, username: 'system' }); const saltRounds = 10; class UserService { constructor(stores, { getLogger, authentication, }, services) { this.passwordResetTimeouts = {}; this.lastSeenSecrets = new Set(); this.logger = getLogger('service/user-service.js'); this.store = stores.userStore; this.eventStore = stores.eventStore; this.accessService = services.accessService; this.resetTokenService = services.resetTokenService; this.emailService = services.emailService; this.sessionService = services.sessionService; this.settingService = services.settingService; if (authentication && authentication.createAdminUser) { process.nextTick(() => this.initAdminUser()); } this.updateLastSeen(); } validatePassword(password) { if (password) { const result = owasp_password_strength_test_1.default.test(password); if (!result.strong) { throw new owasp_validation_error_1.default(result); } else return true; } else { throw new password_undefined_1.default(); } } async initAdminUser() { const userCount = await this.store.count(); if (userCount === 0) { // create default admin user try { const pwd = 'unleash4all'; this.logger.info(`Creating default user "admin" with password "${pwd}"`); const user = await this.store.insert({ username: 'admin', }); const passwordHash = await bcryptjs_1.default.hash(pwd, saltRounds); await this.store.setPasswordHash(user.id, passwordHash); await this.accessService.setUserRootRole(user.id, model_1.RoleName.ADMIN); } catch (e) { this.logger.error('Unable to create default user "admin"'); } } } async getAll() { const users = await this.store.getAll(); const defaultRole = await this.accessService.getRootRole(model_1.RoleName.VIEWER); const userRoles = await this.accessService.getRootRoleForAllUsers(); const usersWithRootRole = users.map((u) => { const rootRole = userRoles.find((r) => r.userId === u.id); const roleId = rootRole ? rootRole.roleId : defaultRole.id; return { ...u, rootRole: roleId }; }); return usersWithRootRole; } async getUser(id) { const roles = await this.accessService.getUserRootRoles(id); const defaultRole = await this.accessService.getRootRole(model_1.RoleName.VIEWER); const roleId = roles.length > 0 ? roles[0].id : defaultRole.id; const user = await this.store.get(id); return { ...user, rootRole: roleId }; } async search(query) { return this.store.search(query); } async getByEmail(email) { return this.store.getByQuery({ email }); } async createUser({ username, email, name, password, rootRole }, updatedBy) { if (!username && !email) { throw new bad_data_error_1.default('You must specify username or email'); } if (email) { joi_1.default.assert(email, joi_1.default.string().email(), 'Email'); } const exists = await this.store.hasUser({ username, email }); if (exists) { throw new Error('User already exists'); } const user = await this.store.insert({ username, email, name, }); await this.accessService.setUserRootRole(user.id, rootRole); if (password) { const passwordHash = await bcryptjs_1.default.hash(password, saltRounds); await this.store.setPasswordHash(user.id, passwordHash); } await this.eventStore.store({ type: events_1.USER_CREATED, createdBy: this.getCreatedBy(updatedBy), data: this.mapUserToData(user), }); return user; } getCreatedBy(updatedBy = systemUser) { return updatedBy.username || updatedBy.email; } mapUserToData(user) { if (!user) { return undefined; } return { id: user.id, name: user.name, username: user.username, email: user.email, }; } async updateUser({ id, name, email, rootRole }, updatedBy) { const preUser = await this.store.get(id); if (email) { joi_1.default.assert(email, joi_1.default.string().email(), 'Email'); } if (rootRole) { await this.accessService.setUserRootRole(id, rootRole); } const payload = { name: name || preUser.name, email: email || preUser.email, }; // Empty updates will throw, so make sure we have something to update. const user = Object.values(payload).some(isDefined_1.isDefined) ? await this.store.update(id, payload) : preUser; await this.eventStore.store({ type: events_1.USER_UPDATED, createdBy: this.getCreatedBy(updatedBy), data: this.mapUserToData(user), preData: this.mapUserToData(preUser), }); return user; } async deleteUser(userId, updatedBy) { const user = await this.store.get(userId); await this.accessService.wipeUserPermissions(userId); await this.sessionService.deleteSessionsForUser(userId); await this.store.delete(userId); await this.eventStore.store({ type: events_1.USER_DELETED, createdBy: this.getCreatedBy(updatedBy), preData: this.mapUserToData(user), }); } async loginUser(usernameOrEmail, password) { const settings = await this.settingService.get(simple_auth_settings_1.simpleAuthSettingsKey); if (settings?.disabled) { throw new disabled_error_1.default('Logging in with username/password has been disabled.'); } const idQuery = (0, is_email_1.default)(usernameOrEmail) ? { email: usernameOrEmail } : { username: usernameOrEmail }; const user = await this.store.getByQuery(idQuery); const passwordHash = await this.store.getPasswordHash(user.id); const match = await bcryptjs_1.default.compare(password, passwordHash); if (match) { await this.store.successfullyLogin(user); return user; } throw new password_mismatch_1.default(); } /** * Used to login users without specifying password. Used when integrating * with external identity providers. * * @param usernameOrEmail * @param autoCreateUser * @returns */ async loginUserWithoutPassword(email, autoCreateUser = false) { return this.loginUserSSO({ email, autoCreate: autoCreateUser }); } async loginUserSSO({ email, name, rootRole, autoCreate = false, }) { let user; try { user = await this.store.getByQuery({ email }); // Update user if autCreate is enabled. if (name && user.name !== name) { user = await this.store.update(user.id, { name, email }); } } catch (e) { // User does not exists. Create if "autoCreate" is enabled if (autoCreate) { user = await this.createUser({ email, name, rootRole: rootRole || model_1.RoleName.EDITOR, }); } else { throw e; } } await this.store.successfullyLogin(user); return user; } async changePassword(userId, password) { this.validatePassword(password); const passwordHash = await bcryptjs_1.default.hash(password, saltRounds); await this.store.setPasswordHash(userId, passwordHash); await this.sessionService.deleteSessionsForUser(userId); } async getUserForToken(token) { const { createdBy, userId } = await this.resetTokenService.isValid(token); const user = await this.getUser(userId); const role = await this.accessService.getRoleData(user.rootRole); return { token, createdBy, email: user.email, name: user.name, id: user.id, role: { id: user.rootRole, description: role.role.description, type: role.role.type, name: role.role.name, }, }; } /** * If the password is a strong password will update password and delete all sessions for the user we're changing the password for * @param token - the token authenticating this request * @param password - new password */ async resetPassword(token, password) { this.validatePassword(password); const user = await this.getUserForToken(token); const allowed = await this.resetTokenService.useAccessToken({ userId: user.id, token, }); if (allowed) { await this.changePassword(user.id, password); await this.sessionService.deleteSessionsForUser(user.id); } else { throw new invalid_token_error_1.default(); } } async createResetPasswordEmail(receiverEmail, user = systemUser) { const receiver = await this.getByEmail(receiverEmail); if (!receiver) { throw new notfound_error_1.default(`Could not find ${receiverEmail}`); } if (this.passwordResetTimeouts[receiver.id]) { return; } const resetLink = await this.resetTokenService.createResetPasswordUrl(receiver.id, user.username || user.email); this.passwordResetTimeouts[receiver.id] = setTimeout(() => { delete this.passwordResetTimeouts[receiver.id]; }, 1000 * 60); // 1 minute await this.emailService.sendResetMail(receiver.name, receiver.email, resetLink.toString()); return resetLink; } async getUserByPersonalAccessToken(secret) { return this.store.getUserByPersonalAccessToken(secret); } async updateLastSeen() { if (this.lastSeenSecrets.size > 0) { const toStore = [...this.lastSeenSecrets]; this.lastSeenSecrets = new Set(); await this.store.markSeenAt(toStore); } this.seenTimer = setTimeout(async () => this.updateLastSeen(), (0, date_fns_1.minutesToMilliseconds)(3)).unref(); } addPATSeen(secret) { this.lastSeenSecrets.add(secret); } destroy() { clearTimeout(this.seenTimer); this.seenTimer = null; } } module.exports = UserService; exports.default = UserService; //# sourceMappingURL=user-service.js.map