/* * SonarQube * Copyright (C) 2009-2017 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.server.user; import com.google.common.base.Joiner; import com.google.common.base.Strings; import java.security.SecureRandom; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Random; import javax.annotation.Nullable; import org.apache.commons.codec.digest.DigestUtils; import org.sonar.api.platform.NewUserHandler; import org.sonar.api.server.ServerSide; import org.sonar.api.utils.System2; import org.sonar.core.util.stream.MoreCollectors; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.organization.OrganizationMemberDto; import org.sonar.db.user.GroupDto; import org.sonar.db.user.UserDto; import org.sonar.db.user.UserGroupDto; import org.sonar.server.organization.DefaultOrganizationProvider; import org.sonar.server.organization.OrganizationCreation; import org.sonar.server.organization.OrganizationFlags; import org.sonar.server.user.index.UserIndexer; import org.sonar.server.usergroups.DefaultGroupFinder; import org.sonar.server.util.Validation; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.collect.Lists.newArrayList; import static java.lang.String.format; import static org.sonar.db.user.UserDto.encryptPassword; import static org.sonar.server.ws.WsUtils.checkFound; import static org.sonar.server.ws.WsUtils.checkRequest; @ServerSide public class UserUpdater { private static final String SQ_AUTHORITY = "sonarqube"; private static final String LOGIN_PARAM = "Login"; private static final String PASSWORD_PARAM = "Password"; private static final String NAME_PARAM = "Name"; private static final String EMAIL_PARAM = "Email"; private static final int LOGIN_MIN_LENGTH = 2; private static final int LOGIN_MAX_LENGTH = 255; private static final int EMAIL_MAX_LENGTH = 100; private static final int NAME_MAX_LENGTH = 200; private final NewUserNotifier newUserNotifier; private final DbClient dbClient; private final UserIndexer userIndexer; private final System2 system2; private final OrganizationFlags organizationFlags; private final DefaultOrganizationProvider defaultOrganizationProvider; private final OrganizationCreation organizationCreation; private final DefaultGroupFinder defaultGroupFinder; public UserUpdater(NewUserNotifier newUserNotifier, DbClient dbClient, UserIndexer userIndexer, System2 system2, OrganizationFlags organizationFlags, DefaultOrganizationProvider defaultOrganizationProvider, OrganizationCreation organizationCreation, DefaultGroupFinder defaultGroupFinder) { this.newUserNotifier = newUserNotifier; this.dbClient = dbClient; this.userIndexer = userIndexer; this.system2 = system2; this.organizationFlags = organizationFlags; this.defaultOrganizationProvider = defaultOrganizationProvider; this.organizationCreation = organizationCreation; this.defaultGroupFinder = defaultGroupFinder; } public UserDto create(DbSession dbSession, NewUser newUser) { String login = newUser.login(); UserDto userDto = dbClient.userDao().selectByLogin(dbSession, newUser.login()); if (userDto == null) { userDto = saveUser(dbSession, createNewUserDto(dbSession, newUser)); } else { reactivateUser(dbSession, userDto, login, newUser); } notifyNewUser(userDto.getLogin(), userDto.getName(), newUser.email()); return userDto; } private void reactivateUser(DbSession dbSession, UserDto existingUser, String login, NewUser newUser) { checkArgument(!existingUser.isActive(), "An active user with login '%s' already exists", login); UpdateUser updateUser = UpdateUser.create(login) .setName(newUser.name()) .setEmail(newUser.email()) .setScmAccounts(newUser.scmAccounts()); if (newUser.password() != null) { updateUser.setPassword(newUser.password()); } if (newUser.externalIdentity() != null) { updateUser.setExternalIdentity(newUser.externalIdentity()); } // Hack to allow to change the password of the user existingUser.setLocal(true); updateUserDto(dbSession, updateUser, existingUser); updateUser(dbSession, existingUser); addUserToDefaultOrganizationAndDefaultGroup(dbSession, existingUser); dbSession.commit(); } public void update(DbSession dbSession, UpdateUser updateUser) { UserDto user = dbClient.userDao().selectByLogin(dbSession, updateUser.login()); checkFound(user, "User with login '%s' has not been found", updateUser.login()); boolean isUserUpdated = updateUserDto(dbSession, updateUser, user); if (!isUserUpdated) { return; } updateUser(dbSession, user); notifyNewUser(user.getLogin(), user.getName(), user.getEmail()); } private UserDto createNewUserDto(DbSession dbSession, NewUser newUser) { UserDto userDto = new UserDto(); List<String> messages = newArrayList(); String login = newUser.login(); if (validateLoginFormat(login, messages)) { userDto.setLogin(login); } String name = newUser.name(); if (validateNameFormat(name, messages)) { userDto.setName(name); } String email = newUser.email(); if (email != null && validateEmailFormat(email, messages)) { userDto.setEmail(email); } String password = newUser.password(); if (password != null && validatePasswords(password, messages)) { setEncryptedPassWord(password, userDto); } List<String> scmAccounts = sanitizeScmAccounts(newUser.scmAccounts()); if (scmAccounts != null && !scmAccounts.isEmpty() && validateScmAccounts(dbSession, scmAccounts, login, email, null, messages)) { userDto.setScmAccounts(scmAccounts); } setExternalIdentity(userDto, newUser.externalIdentity()); checkRequest(messages.isEmpty(), messages); return userDto; } private boolean updateUserDto(DbSession dbSession, UpdateUser updateUser, UserDto userDto) { List<String> messages = newArrayList(); boolean changed = updateName(updateUser, userDto, messages); changed |= updateEmail(updateUser, userDto, messages); changed |= updateExternalIdentity(updateUser, userDto); changed |= updatePassword(updateUser, userDto, messages); changed |= updateScmAccounts(dbSession, updateUser, userDto, messages); checkRequest(messages.isEmpty(), messages); return changed; } private static boolean updateName(UpdateUser updateUser, UserDto userDto, List<String> messages) { String name = updateUser.name(); if (updateUser.isNameChanged() && validateNameFormat(name, messages) && !Objects.equals(userDto.getName(), name)) { userDto.setName(name); return true; } return false; } private static boolean updateEmail(UpdateUser updateUser, UserDto userDto, List<String> messages) { String email = updateUser.email(); if (updateUser.isEmailChanged() && validateEmailFormat(email, messages) && !Objects.equals(userDto.getEmail(), email)) { userDto.setEmail(email); return true; } return false; } private static boolean updateExternalIdentity(UpdateUser updateUser, UserDto userDto) { ExternalIdentity externalIdentity = updateUser.externalIdentity(); if (updateUser.isExternalIdentityChanged() && !isSameExternalIdentity(userDto, externalIdentity)) { setExternalIdentity(userDto, externalIdentity); userDto.setSalt(null); userDto.setCryptedPassword(null); return true; } return false; } private static boolean updatePassword(UpdateUser updateUser, UserDto userDto, List<String> messages) { String password = updateUser.password(); if (!updateUser.isExternalIdentityChanged() && updateUser.isPasswordChanged() && validatePasswords(password, messages) && checkPasswordChangeAllowed(userDto, messages)) { setEncryptedPassWord(password, userDto); return true; } return false; } private boolean updateScmAccounts(DbSession dbSession, UpdateUser updateUser, UserDto userDto, List<String> messages) { String email = updateUser.email(); List<String> scmAccounts = sanitizeScmAccounts(updateUser.scmAccounts()); List<String> existingScmAccounts = userDto.getScmAccountsAsList(); if (updateUser.isScmAccountsChanged() && !(existingScmAccounts.containsAll(scmAccounts) && scmAccounts.containsAll(existingScmAccounts))) { if (!scmAccounts.isEmpty()) { String newOrOldEmail = email != null ? email : userDto.getEmail(); if (validateScmAccounts(dbSession, scmAccounts, userDto.getLogin(), newOrOldEmail, userDto, messages)) { userDto.setScmAccounts(scmAccounts); } } else { userDto.setScmAccounts((String) null); } return true; } return false; } private static boolean isSameExternalIdentity(UserDto dto, @Nullable ExternalIdentity externalIdentity) { return (externalIdentity == null && dto.getExternalIdentity() == null) || (externalIdentity != null && Objects.equals(dto.getExternalIdentity(), externalIdentity.getId()) && Objects.equals(dto.getExternalIdentityProvider(), externalIdentity.getProvider())); } private static void setExternalIdentity(UserDto dto, @Nullable ExternalIdentity externalIdentity) { if (externalIdentity == null) { dto.setExternalIdentity(dto.getLogin()); dto.setExternalIdentityProvider(SQ_AUTHORITY); dto.setLocal(true); } else { dto.setExternalIdentity(externalIdentity.getId()); dto.setExternalIdentityProvider(externalIdentity.getProvider()); dto.setLocal(false); } } private static boolean checkNotEmptyParam(@Nullable String value, String param, List<String> messages) { if (isNullOrEmpty(value)) { messages.add(format(Validation.CANT_BE_EMPTY_MESSAGE, param)); return false; } return true; } private static boolean validateLoginFormat(@Nullable String login, List<String> messages) { boolean isValid = checkNotEmptyParam(login, LOGIN_PARAM, messages); if (!isNullOrEmpty(login)) { if (login.length() < LOGIN_MIN_LENGTH) { messages.add(format(Validation.IS_TOO_SHORT_MESSAGE, LOGIN_PARAM, LOGIN_MIN_LENGTH)); return false; } else if (login.length() > LOGIN_MAX_LENGTH) { messages.add(format(Validation.IS_TOO_LONG_MESSAGE, LOGIN_PARAM, LOGIN_MAX_LENGTH)); return false; } else if (!login.matches("\\A\\w[\\w\\.\\-_@]+\\z")) { messages.add("Use only letters, numbers, and .-_@ please."); return false; } } return isValid; } private static boolean validateNameFormat(@Nullable String name, List<String> messages) { boolean isValid = checkNotEmptyParam(name, NAME_PARAM, messages); if (name != null && name.length() > NAME_MAX_LENGTH) { messages.add(format(Validation.IS_TOO_LONG_MESSAGE, NAME_PARAM, 200)); return false; } return isValid; } private static boolean validateEmailFormat(@Nullable String email, List<String> messages) { if (email != null && email.length() > EMAIL_MAX_LENGTH) { messages.add(format(Validation.IS_TOO_LONG_MESSAGE, EMAIL_PARAM, 100)); return false; } return true; } private static boolean checkPasswordChangeAllowed(UserDto userDto, List<String> messages) { if (!userDto.isLocal()) { messages.add("Password cannot be changed when external authentication is used"); return false; } return true; } private static boolean validatePasswords(@Nullable String password, List<String> messages) { if (password == null || password.length() == 0) { messages.add(format(Validation.CANT_BE_EMPTY_MESSAGE, PASSWORD_PARAM)); return false; } return true; } private boolean validateScmAccounts(DbSession dbSession, List<String> scmAccounts, @Nullable String login, @Nullable String email, @Nullable UserDto existingUser, List<String> messages) { boolean isValid = true; for (String scmAccount : scmAccounts) { if (scmAccount.equals(login) || scmAccount.equals(email)) { messages.add("Login and email are automatically considered as SCM accounts"); isValid = false; } else { List<UserDto> matchingUsers = dbClient.userDao().selectByScmAccountOrLoginOrEmail(dbSession, scmAccount); List<String> matchingUsersWithoutExistingUser = newArrayList(); for (UserDto matchingUser : matchingUsers) { if (existingUser != null && matchingUser.getId().equals(existingUser.getId())) { continue; } matchingUsersWithoutExistingUser.add(matchingUser.getName() + " (" + matchingUser.getLogin() + ")"); } if (!matchingUsersWithoutExistingUser.isEmpty()) { messages.add(format("The scm account '%s' is already used by user(s) : '%s'", scmAccount, Joiner.on(", ").join(matchingUsersWithoutExistingUser))); isValid = false; } } } return isValid; } private static List<String> sanitizeScmAccounts(@Nullable List<String> scmAccounts) { if (scmAccounts != null) { return scmAccounts.stream().filter(s -> !Strings.isNullOrEmpty(s)).collect(MoreCollectors.toList()); } return Collections.emptyList(); } private UserDto saveUser(DbSession dbSession, UserDto userDto) { long now = system2.now(); userDto.setActive(true).setCreatedAt(now).setUpdatedAt(now); UserDto res = dbClient.userDao().insert(dbSession, userDto); addUserToDefaultOrganizationAndDefaultGroup(dbSession, userDto); organizationCreation.createForUser(dbSession, userDto); dbSession.commit(); userIndexer.index(userDto.getLogin()); return res; } private void updateUser(DbSession dbSession, UserDto userDto) { long now = system2.now(); userDto.setActive(true).setUpdatedAt(now); dbClient.userDao().update(dbSession, userDto); dbSession.commit(); userIndexer.index(userDto.getLogin()); } private static void setEncryptedPassWord(String password, UserDto userDto) { Random random = new SecureRandom(); byte[] salt = new byte[32]; random.nextBytes(salt); String saltHex = DigestUtils.sha1Hex(salt); userDto.setSalt(saltHex); userDto.setCryptedPassword(encryptPassword(password, saltHex)); } private void notifyNewUser(String login, String name, String email) { newUserNotifier.onNewUser(NewUserHandler.Context.builder() .setLogin(login) .setName(name) .setEmail(email) .build()); } private static boolean isUserAlreadyMemberOfDefaultGroup(GroupDto defaultGroup, List<GroupDto> userGroups) { return userGroups.stream().anyMatch(group -> defaultGroup.getId().equals(group.getId())); } private void addUserToDefaultOrganizationAndDefaultGroup(DbSession dbSession, UserDto userDto) { if (organizationFlags.isEnabled(dbSession)) { return; } addUserToDefaultOrganization(dbSession, userDto); addDefaultGroup(dbSession, userDto); } private void addUserToDefaultOrganization(DbSession dbSession, UserDto userDto) { String defOrgUuid = defaultOrganizationProvider.get().getUuid(); dbClient.organizationMemberDao().insert(dbSession, new OrganizationMemberDto().setOrganizationUuid(defOrgUuid).setUserId(userDto.getId())); } private void addDefaultGroup(DbSession dbSession, UserDto userDto) { String defOrgUuid = defaultOrganizationProvider.get().getUuid(); List<GroupDto> userGroups = dbClient.groupDao().selectByUserLogin(dbSession, userDto.getLogin()); GroupDto defaultGroup = defaultGroupFinder.findDefaultGroup(dbSession, defOrgUuid); if (isUserAlreadyMemberOfDefaultGroup(defaultGroup, userGroups)) { return; } dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setUserId(userDto.getId()).setGroupId(defaultGroup.getId())); } }