123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- /*
- * SonarQube
- * Copyright (C) 2009-2021 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.authentication;
-
- import com.google.common.collect.Sets;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.HashSet;
- import java.util.List;
- import java.util.Map;
- import java.util.Objects;
- import java.util.Optional;
- import java.util.Set;
- import java.util.function.Consumer;
- import javax.annotation.CheckForNull;
- import javax.annotation.Nullable;
- import org.sonar.api.server.authentication.IdentityProvider;
- import org.sonar.api.server.authentication.UserIdentity;
- import org.sonar.api.utils.log.Logger;
- import org.sonar.api.utils.log.Loggers;
- import org.sonar.db.DbClient;
- import org.sonar.db.DbSession;
- import org.sonar.db.user.GroupDto;
- import org.sonar.db.user.UserDto;
- import org.sonar.db.user.UserGroupDto;
- import org.sonar.server.authentication.event.AuthenticationEvent;
- import org.sonar.server.authentication.event.AuthenticationException;
- import org.sonar.server.user.ExternalIdentity;
- import org.sonar.server.user.NewUser;
- import org.sonar.server.user.UpdateUser;
- import org.sonar.server.user.UserUpdater;
- import org.sonar.server.usergroups.DefaultGroupFinder;
-
- import static java.lang.String.format;
- import static java.util.Collections.singletonList;
- import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
-
- public class UserRegistrarImpl implements UserRegistrar {
-
- private static final Logger LOGGER = Loggers.get(UserRegistrarImpl.class);
- private static final String SQ_AUTHORITY = "sonarqube";
-
- private final DbClient dbClient;
- private final UserUpdater userUpdater;
- private final DefaultGroupFinder defaultGroupFinder;
-
- public UserRegistrarImpl(DbClient dbClient, UserUpdater userUpdater, DefaultGroupFinder defaultGroupFinder) {
- this.dbClient = dbClient;
- this.userUpdater = userUpdater;
- this.defaultGroupFinder = defaultGroupFinder;
- }
-
- @Override
- public UserDto register(UserRegistration registration) {
- try (DbSession dbSession = dbClient.openSession(false)) {
- UserDto userDto = getUser(dbSession, registration.getUserIdentity(), registration.getProvider(), registration.getSource());
- if (userDto == null) {
- return registerNewUser(dbSession, null, registration);
- }
- if (!userDto.isActive()) {
- return registerNewUser(dbSession, userDto, registration);
- }
- return registerExistingUser(dbSession, userDto, registration);
- }
- }
-
- @CheckForNull
- private UserDto getUser(DbSession dbSession, UserIdentity userIdentity, IdentityProvider provider, AuthenticationEvent.Source source) {
- // First, try to authenticate using the external ID
- UserDto user = dbClient.userDao().selectByExternalIdAndIdentityProvider(dbSession, getProviderIdOrProviderLogin(userIdentity), provider.getKey());
- if (user != null) {
- return user;
- }
-
- // Then, try with the external login, for instance when external ID has changed or is not used by the provider
- user = dbClient.userDao().selectByExternalLoginAndIdentityProvider(dbSession, userIdentity.getProviderLogin(), provider.getKey());
- if (user == null) {
- return null;
- }
- // all gitlab users have an external ID, so the other two authentication methods should never be used
- if (provider.getKey().equals("gitlab")) {
- throw loginAlreadyUsedException(userIdentity, source);
- }
-
- validateEmailToAvoidLoginRecycling(userIdentity, user, source);
- updateUserExternalId(dbSession, user, userIdentity);
- return user;
- }
-
- private void updateUserExternalId(DbSession dbSession, UserDto user, UserIdentity userIdentity) {
- String externalId = userIdentity.getProviderId();
- if (externalId != null) {
- user.setExternalId(externalId);
- dbClient.userDao().update(dbSession, user);
- }
- }
-
- private static void validateEmailToAvoidLoginRecycling(UserIdentity userIdentity, UserDto user, AuthenticationEvent.Source source) {
- String dbEmail = user.getEmail();
-
- if (dbEmail == null) {
- return;
- }
-
- String externalEmail = userIdentity.getEmail();
-
- if (!dbEmail.equals(externalEmail)) {
- LOGGER.warn("User with login '{}' tried to login with email '{}' which doesn't match the email on record '{}'",
- userIdentity.getProviderLogin(), externalEmail, dbEmail);
- throw loginAlreadyUsedException(userIdentity, source);
- }
- }
-
- private static AuthenticationException loginAlreadyUsedException(UserIdentity userIdentity, AuthenticationEvent.Source source) {
- return authException(
- userIdentity,
- source,
- String.format("Login '%s' is already used", userIdentity.getProviderLogin()),
- String.format("You can't sign up because login '%s' is already used by an existing user.", userIdentity.getProviderLogin()));
- }
-
- private static AuthenticationException authException(UserIdentity userIdentity, AuthenticationEvent.Source source, String message, String publicMessage) {
- return AuthenticationException.newBuilder()
- .setSource(source)
- .setLogin(userIdentity.getProviderLogin())
- .setMessage(message)
- .setPublicMessage(publicMessage)
- .build();
- }
-
- private UserDto registerNewUser(DbSession dbSession, @Nullable UserDto disabledUser, UserRegistration authenticatorParameters) {
- Optional<UserDto> otherUserToIndex = detectEmailUpdate(dbSession, authenticatorParameters, disabledUser != null ? disabledUser.getUuid() : null);
- NewUser newUser = createNewUser(authenticatorParameters);
- if (disabledUser == null) {
- return userUpdater.createAndCommit(dbSession, newUser, beforeCommit(dbSession, authenticatorParameters), toArray(otherUserToIndex));
- }
- return userUpdater.reactivateAndCommit(dbSession, disabledUser, newUser, beforeCommit(dbSession, authenticatorParameters), toArray(otherUserToIndex));
- }
-
- private UserDto registerExistingUser(DbSession dbSession, UserDto userDto, UserRegistration authenticatorParameters) {
- UpdateUser update = new UpdateUser()
- .setEmail(authenticatorParameters.getUserIdentity().getEmail())
- .setName(authenticatorParameters.getUserIdentity().getName())
- .setExternalIdentity(new ExternalIdentity(
- authenticatorParameters.getProvider().getKey(),
- authenticatorParameters.getUserIdentity().getProviderLogin(),
- authenticatorParameters.getUserIdentity().getProviderId()));
- Optional<UserDto> otherUserToIndex = detectEmailUpdate(dbSession, authenticatorParameters, userDto.getUuid());
- userUpdater.updateAndCommit(dbSession, userDto, update, beforeCommit(dbSession, authenticatorParameters), toArray(otherUserToIndex));
- return userDto;
- }
-
- private Consumer<UserDto> beforeCommit(DbSession dbSession, UserRegistration authenticatorParameters) {
- return user -> syncGroups(dbSession, authenticatorParameters.getUserIdentity(), user);
- }
-
- private Optional<UserDto> detectEmailUpdate(DbSession dbSession, UserRegistration authenticatorParameters, @Nullable String authenticatingUserUuid) {
- String email = authenticatorParameters.getUserIdentity().getEmail();
- if (email == null) {
- return Optional.empty();
- }
- List<UserDto> existingUsers = dbClient.userDao().selectByEmail(dbSession, email);
- if (existingUsers.isEmpty()) {
- return Optional.empty();
- }
- if (existingUsers.size() > 1) {
- throw generateExistingEmailError(authenticatorParameters, email);
- }
-
- UserDto existingUser = existingUsers.get(0);
- if (existingUser == null || existingUser.getUuid().equals(authenticatingUserUuid)) {
- return Optional.empty();
- }
- throw generateExistingEmailError(authenticatorParameters, email);
- }
-
- private void syncGroups(DbSession dbSession, UserIdentity userIdentity, UserDto userDto) {
- if (!userIdentity.shouldSyncGroups()) {
- return;
- }
- String userLogin = userDto.getLogin();
- Set<String> userGroups = new HashSet<>(dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, singletonList(userLogin)).get(userLogin));
- Set<String> identityGroups = userIdentity.getGroups();
- LOGGER.debug("List of groups returned by the identity provider '{}'", identityGroups);
-
- Collection<String> groupsToAdd = Sets.difference(identityGroups, userGroups);
- Collection<String> groupsToRemove = Sets.difference(userGroups, identityGroups);
- Collection<String> allGroups = new ArrayList<>(groupsToAdd);
- allGroups.addAll(groupsToRemove);
- Map<String, GroupDto> groupsByName = dbClient.groupDao().selectByNames(dbSession, allGroups)
- .stream()
- .collect(uniqueIndex(GroupDto::getName));
-
- addGroups(dbSession, userDto, groupsToAdd, groupsByName);
- removeGroups(dbSession, userDto, groupsToRemove, groupsByName);
- }
-
- private void addGroups(DbSession dbSession, UserDto userDto, Collection<String> groupsToAdd, Map<String, GroupDto> groupsByName) {
- groupsToAdd.stream().map(groupsByName::get).filter(Objects::nonNull).forEach(
- groupDto -> {
- LOGGER.debug("Adding group '{}' to user '{}'", groupDto.getName(), userDto.getLogin());
- dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setGroupUuid(groupDto.getUuid()).setUserUuid(userDto.getUuid()));
- });
- }
-
- private void removeGroups(DbSession dbSession, UserDto userDto, Collection<String> groupsToRemove, Map<String, GroupDto> groupsByName) {
- Optional<GroupDto> defaultGroup = getDefaultGroup(dbSession);
- groupsToRemove.stream().map(groupsByName::get)
- .filter(Objects::nonNull)
- // user should be member of default group only when organizations are disabled, as the IdentityProvider API doesn't handle yet
- // organizations
- .filter(group -> !defaultGroup.isPresent() || !group.getUuid().equals(defaultGroup.get().getUuid()))
- .forEach(groupDto -> {
- LOGGER.debug("Removing group '{}' from user '{}'", groupDto.getName(), userDto.getLogin());
- dbClient.userGroupDao().delete(dbSession, groupDto.getUuid(), userDto.getUuid());
- });
- }
-
- private Optional<GroupDto> getDefaultGroup(DbSession dbSession) {
- return Optional.of(defaultGroupFinder.findDefaultGroup(dbSession));
- }
-
- private static NewUser createNewUser(UserRegistration authenticatorParameters) {
- String identityProviderKey = authenticatorParameters.getProvider().getKey();
- if (!authenticatorParameters.getProvider().allowsUsersToSignUp()) {
- throw AuthenticationException.newBuilder()
- .setSource(authenticatorParameters.getSource())
- .setLogin(authenticatorParameters.getUserIdentity().getProviderLogin())
- .setMessage(format("User signup disabled for provider '%s'", identityProviderKey))
- .setPublicMessage(format("'%s' users are not allowed to sign up", identityProviderKey))
- .build();
- }
- String providerLogin = authenticatorParameters.getUserIdentity().getProviderLogin();
- return NewUser.builder()
- .setLogin(SQ_AUTHORITY.equals(identityProviderKey) ? providerLogin : null)
- .setEmail(authenticatorParameters.getUserIdentity().getEmail())
- .setName(authenticatorParameters.getUserIdentity().getName())
- .setExternalIdentity(
- new ExternalIdentity(
- identityProviderKey,
- providerLogin,
- authenticatorParameters.getUserIdentity().getProviderId()))
- .build();
- }
-
- private static UserDto[] toArray(Optional<UserDto> userDto) {
- return userDto.map(u -> new UserDto[] {u}).orElse(new UserDto[] {});
- }
-
- private static AuthenticationException generateExistingEmailError(UserRegistration authenticatorParameters, String email) {
- return AuthenticationException.newBuilder()
- .setSource(authenticatorParameters.getSource())
- .setLogin(authenticatorParameters.getUserIdentity().getProviderLogin())
- .setMessage(format("Email '%s' is already used", email))
- .setPublicMessage(
- "This account is already associated with another authentication method. "
- + "Sign in using the current authentication method, "
- + "or contact your administrator to transfer your account to a different authentication method.")
- .build();
- }
-
- private static String getProviderIdOrProviderLogin(UserIdentity userIdentity) {
- String providerId = userIdentity.getProviderId();
- return providerId == null ? userIdentity.getProviderLogin() : providerId;
- }
-
- }
|