123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453 |
- /*
- * SonarQube
- * Copyright (C) 2009-2023 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.util.ArrayList;
- import java.util.HashSet;
- import java.util.List;
- import java.util.Objects;
- import java.util.function.Consumer;
- import java.util.regex.Pattern;
- import java.util.stream.Collectors;
- import javax.annotation.Nullable;
- import javax.inject.Inject;
- import org.apache.commons.lang.math.RandomUtils;
- import org.sonar.api.config.Configuration;
- import org.sonar.api.platform.NewUserHandler;
- import org.sonar.api.server.ServerSide;
- import org.sonar.db.DbClient;
- import org.sonar.db.DbSession;
- import org.sonar.db.audit.AuditPersister;
- import org.sonar.db.audit.model.SecretNewValue;
- import org.sonar.db.user.GroupDto;
- import org.sonar.db.user.UserDto;
- import org.sonar.db.user.UserGroupDto;
- import org.sonar.server.authentication.CredentialsLocalAuthentication;
- 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 java.util.Collections.emptyList;
- import static org.sonar.api.CoreProperties.DEFAULT_ISSUE_ASSIGNEE;
- import static org.sonar.core.util.Slug.slugify;
- import static org.sonar.server.exceptions.BadRequestException.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 Pattern START_WITH_SPECIFIC_AUTHORIZED_CHARACTERS = Pattern.compile("\\w+");
- private static final Pattern CONTAINS_ONLY_AUTHORIZED_CHARACTERS = Pattern.compile("\\A\\w[\\w\\.\\-@]+\\z");
-
- public static final int LOGIN_MIN_LENGTH = 2;
- public static final int LOGIN_MAX_LENGTH = 255;
- public static final int EMAIL_MAX_LENGTH = 100;
- public static final int NAME_MAX_LENGTH = 200;
-
- private final NewUserNotifier newUserNotifier;
- private final DbClient dbClient;
- private final DefaultGroupFinder defaultGroupFinder;
- private final AuditPersister auditPersister;
- private final CredentialsLocalAuthentication localAuthentication;
-
- @Inject
- public UserUpdater(NewUserNotifier newUserNotifier, DbClient dbClient, DefaultGroupFinder defaultGroupFinder, Configuration config,
- AuditPersister auditPersister, CredentialsLocalAuthentication localAuthentication) {
- this.newUserNotifier = newUserNotifier;
- this.dbClient = dbClient;
- this.defaultGroupFinder = defaultGroupFinder;
- this.auditPersister = auditPersister;
- this.localAuthentication = localAuthentication;
- }
-
- public UserDto createAndCommit(DbSession dbSession, NewUser newUser, Consumer<UserDto> beforeCommit, UserDto... otherUsersToIndex) {
- UserDto userDto = saveUser(dbSession, createDto(dbSession, newUser));
- return commitUser(dbSession, userDto, beforeCommit, otherUsersToIndex);
- }
-
- public UserDto reactivateAndCommit(DbSession dbSession, UserDto disabledUser, NewUser newUser, Consumer<UserDto> beforeCommit, UserDto... otherUsersToIndex) {
- checkArgument(!disabledUser.isActive(), "An active user with login '%s' already exists", disabledUser.getLogin());
- reactivateUser(dbSession, disabledUser, newUser);
- return commitUser(dbSession, disabledUser, beforeCommit, otherUsersToIndex);
- }
-
- private void reactivateUser(DbSession dbSession, UserDto reactivatedUser, NewUser newUser) {
- UpdateUser updateUser = new UpdateUser()
- .setName(newUser.name())
- .setEmail(newUser.email())
- .setScmAccounts(newUser.scmAccounts())
- .setExternalIdentity(newUser.externalIdentity());
- String login = newUser.login();
- if (login != null) {
- updateUser.setLogin(login);
- }
- String password = newUser.password();
- if (password != null) {
- updateUser.setPassword(password);
- }
- updateDto(dbSession, updateUser, reactivatedUser);
- updateUser(dbSession, reactivatedUser);
- addUserToDefaultGroup(dbSession, reactivatedUser);
- }
-
- public void updateAndCommit(DbSession dbSession, UserDto dto, UpdateUser updateUser, Consumer<UserDto> beforeCommit, UserDto... otherUsersToIndex) {
- boolean isUserUpdated = updateDto(dbSession, updateUser, dto);
- if (isUserUpdated) {
- // at least one change. Database must be updated and Elasticsearch re-indexed
- updateUser(dbSession, dto);
- commitUser(dbSession, dto, beforeCommit, otherUsersToIndex);
- } else {
- // no changes but still execute the consumer
- beforeCommit.accept(dto);
- dbSession.commit();
- }
- }
-
- private UserDto commitUser(DbSession dbSession, UserDto userDto, Consumer<UserDto> beforeCommit, UserDto... otherUsersToIndex) {
- beforeCommit.accept(userDto);
- dbSession.commit();
- notifyNewUser(userDto.getLogin(), userDto.getName(), userDto.getEmail());
- return userDto;
- }
-
- private UserDto createDto(DbSession dbSession, NewUser newUser) {
- UserDto userDto = new UserDto();
- List<String> messages = new ArrayList<>();
-
- String login = newUser.login();
- if (isNullOrEmpty(login)) {
- userDto.setLogin(generateUniqueLogin(dbSession, newUser.name()));
- } else if (validateLoginFormat(login, messages)) {
- checkLoginUniqueness(dbSession, login);
- 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)) {
- localAuthentication.storeHashPassword(userDto, password);
- }
-
- List<String> scmAccounts = sanitizeScmAccounts(newUser.scmAccounts());
- if (scmAccounts != null && !scmAccounts.isEmpty() && validateScmAccounts(dbSession, scmAccounts, login, email, null, messages)) {
- userDto.setScmAccounts(scmAccounts);
- }
-
- setExternalIdentity(dbSession, userDto, newUser.externalIdentity());
-
- checkRequest(messages.isEmpty(), messages);
- return userDto;
- }
-
- private String generateUniqueLogin(DbSession dbSession, String userName) {
- String slugName = slugify(userName);
- for (int i = 0; i < 10; i++) {
- String login = slugName + RandomUtils.nextInt(100_000);
- UserDto existingUser = dbClient.userDao().selectByLogin(dbSession, login);
- if (existingUser == null) {
- return login;
- }
- }
- throw new IllegalStateException("Cannot create unique login for user name " + userName);
- }
-
- private boolean updateDto(DbSession dbSession, UpdateUser update, UserDto dto) {
- List<String> messages = newArrayList();
- boolean changed = updateLogin(dbSession, update, dto, messages);
- changed |= updateName(update, dto, messages);
- changed |= updateEmail(update, dto, messages);
- changed |= updateExternalIdentity(dbSession, update, dto);
- changed |= updatePassword(dbSession, update, dto, messages);
- changed |= updateScmAccounts(dbSession, update, dto, messages);
- checkRequest(messages.isEmpty(), messages);
- return changed;
- }
-
- private boolean updateLogin(DbSession dbSession, UpdateUser updateUser, UserDto userDto, List<String> messages) {
- String newLogin = updateUser.login();
- if (!updateUser.isLoginChanged() || !validateLoginFormat(newLogin, messages) || Objects.equals(userDto.getLogin(), newLogin)) {
- return false;
- }
- checkLoginUniqueness(dbSession, newLogin);
- dbClient.propertiesDao().selectByKeyAndMatchingValue(dbSession, DEFAULT_ISSUE_ASSIGNEE, userDto.getLogin())
- .forEach(p -> dbClient.propertiesDao().saveProperty(p.setValue(newLogin)));
- userDto.setLogin(newLogin);
- if (userDto.isLocal() || SQ_AUTHORITY.equals(userDto.getExternalIdentityProvider())) {
- userDto.setExternalLogin(newLogin);
- userDto.setExternalId(newLogin);
- }
- return true;
- }
-
- 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 boolean updateExternalIdentity(DbSession dbSession, UpdateUser updateUser, UserDto userDto) {
- ExternalIdentity externalIdentity = updateUser.externalIdentity();
- if (updateUser.isExternalIdentityChanged() && !isSameExternalIdentity(userDto, externalIdentity)) {
- setExternalIdentity(dbSession, userDto, externalIdentity);
- return true;
- }
- return false;
- }
-
- private boolean updatePassword(DbSession dbSession, UpdateUser updateUser, UserDto userDto, List<String> messages) {
- String password = updateUser.password();
- if (updateUser.isPasswordChanged() && validatePasswords(password, messages) && checkPasswordChangeAllowed(userDto, messages)) {
- localAuthentication.storeHashPassword(userDto, password);
- userDto.setResetPassword(false);
- auditPersister.updateUserPassword(dbSession, new SecretNewValue("userLogin", userDto.getLogin()));
- 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.getSortedScmAccounts();
- 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(emptyList());
- }
- return true;
- }
- return false;
- }
-
- private static boolean isSameExternalIdentity(UserDto dto, @Nullable ExternalIdentity externalIdentity) {
- return externalIdentity != null
- && !dto.isLocal()
- && Objects.equals(dto.getExternalId(), externalIdentity.getId())
- && Objects.equals(dto.getExternalLogin(), externalIdentity.getLogin())
- && Objects.equals(dto.getExternalIdentityProvider(), externalIdentity.getProvider());
- }
-
- private void setExternalIdentity(DbSession dbSession, UserDto dto, @Nullable ExternalIdentity externalIdentity) {
- if (externalIdentity == null) {
- dto.setExternalLogin(dto.getLogin());
- dto.setExternalIdentityProvider(SQ_AUTHORITY);
- dto.setExternalId(dto.getLogin());
- dto.setLocal(true);
- } else {
- dto.setExternalLogin(externalIdentity.getLogin());
- dto.setExternalIdentityProvider(externalIdentity.getProvider());
- dto.setExternalId(externalIdentity.getId());
- dto.setLocal(false);
- dto.setSalt(null);
- dto.setCryptedPassword(null);
- }
- UserDto existingUser = dbClient.userDao().selectByExternalIdAndIdentityProvider(dbSession, dto.getExternalId(), dto.getExternalIdentityProvider());
- checkArgument(existingUser == null || Objects.equals(dto.getUuid(), existingUser.getUuid()),
- "A user with provider id '%s' and identity provider '%s' already exists", dto.getExternalId(), dto.getExternalIdentityProvider());
- }
-
- 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 (isValid) {
- 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 (!startWithUnderscoreOrAlphanumeric(login)) {
- messages.add("Login should start with _ or alphanumeric.");
- return false;
- } else if (!CONTAINS_ONLY_AUTHORIZED_CHARACTERS.matcher(login).matches()) {
- messages.add("Login should contain only letters, numbers, and .-_@");
- return false;
- }
- }
- return isValid;
- }
-
- private static boolean startWithUnderscoreOrAlphanumeric(String login) {
- String firstCharacter = login.substring(0, 1);
- if ("_".equals(firstCharacter)) {
- return true;
- }
- return START_WITH_SPECIFIC_AUTHORIZED_CHARACTERS.matcher(firstCharacter).matches();
- }
-
- 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.getUuid().equals(existingUser.getUuid())) {
- continue;
- }
- matchingUsersWithoutExistingUser.add(getNameOrLogin(matchingUser) + " (" + 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 String getNameOrLogin(UserDto user) {
- String name = user.getName();
- return name != null ? name : user.getLogin();
- }
-
- private static List<String> sanitizeScmAccounts(@Nullable List<String> scmAccounts) {
- if (scmAccounts != null) {
- return new HashSet<>(scmAccounts).stream()
- .map(Strings::emptyToNull)
- .filter(Objects::nonNull)
- .sorted(String::compareToIgnoreCase)
- .toList();
- }
- return emptyList();
- }
-
- private void checkLoginUniqueness(DbSession dbSession, String login) {
- UserDto existingUser = dbClient.userDao().selectByLogin(dbSession, login);
- checkArgument(existingUser == null, "A user with login '%s' already exists", login);
- }
-
- private UserDto saveUser(DbSession dbSession, UserDto userDto) {
- userDto.setActive(true);
- UserDto res = dbClient.userDao().insert(dbSession, userDto);
- addUserToDefaultGroup(dbSession, userDto);
- return res;
- }
-
- private void updateUser(DbSession dbSession, UserDto dto) {
- dto.setActive(true);
- dbClient.userDao().update(dbSession, dto);
- }
-
- private void notifyNewUser(String login, String name, @Nullable 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.getUuid().equals(group.getUuid()));
- }
-
- private void addUserToDefaultGroup(DbSession dbSession, UserDto userDto) {
- addDefaultGroup(dbSession, userDto);
- }
-
- private void addDefaultGroup(DbSession dbSession, UserDto userDto) {
- List<GroupDto> userGroups = dbClient.groupDao().selectByUserLogin(dbSession, userDto.getLogin());
- GroupDto defaultGroup = defaultGroupFinder.findDefaultGroup(dbSession);
- if (isUserAlreadyMemberOfDefaultGroup(defaultGroup, userGroups)) {
- return;
- }
- dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setUserUuid(userDto.getUuid()).setGroupUuid(defaultGroup.getUuid()),
- defaultGroup.getName(), userDto.getLogin());
- }
- }
|