You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

UserRegistrarImpl.java 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2021 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.server.authentication;
  21. import com.google.common.collect.Sets;
  22. import java.util.ArrayList;
  23. import java.util.Collection;
  24. import java.util.HashSet;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.Objects;
  28. import java.util.Optional;
  29. import java.util.Set;
  30. import java.util.function.Consumer;
  31. import javax.annotation.CheckForNull;
  32. import javax.annotation.Nullable;
  33. import org.sonar.api.server.authentication.IdentityProvider;
  34. import org.sonar.api.server.authentication.UserIdentity;
  35. import org.sonar.api.utils.log.Logger;
  36. import org.sonar.api.utils.log.Loggers;
  37. import org.sonar.db.DbClient;
  38. import org.sonar.db.DbSession;
  39. import org.sonar.db.user.GroupDto;
  40. import org.sonar.db.user.UserDto;
  41. import org.sonar.db.user.UserGroupDto;
  42. import org.sonar.server.authentication.event.AuthenticationEvent;
  43. import org.sonar.server.authentication.event.AuthenticationException;
  44. import org.sonar.server.user.ExternalIdentity;
  45. import org.sonar.server.user.NewUser;
  46. import org.sonar.server.user.UpdateUser;
  47. import org.sonar.server.user.UserUpdater;
  48. import org.sonar.server.usergroups.DefaultGroupFinder;
  49. import static java.lang.String.format;
  50. import static java.util.Collections.singletonList;
  51. import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
  52. public class UserRegistrarImpl implements UserRegistrar {
  53. private static final Logger LOGGER = Loggers.get(UserRegistrarImpl.class);
  54. private static final String SQ_AUTHORITY = "sonarqube";
  55. private final DbClient dbClient;
  56. private final UserUpdater userUpdater;
  57. private final DefaultGroupFinder defaultGroupFinder;
  58. public UserRegistrarImpl(DbClient dbClient, UserUpdater userUpdater, DefaultGroupFinder defaultGroupFinder) {
  59. this.dbClient = dbClient;
  60. this.userUpdater = userUpdater;
  61. this.defaultGroupFinder = defaultGroupFinder;
  62. }
  63. @Override
  64. public UserDto register(UserRegistration registration) {
  65. try (DbSession dbSession = dbClient.openSession(false)) {
  66. UserDto userDto = getUser(dbSession, registration.getUserIdentity(), registration.getProvider(), registration.getSource());
  67. if (userDto == null) {
  68. return registerNewUser(dbSession, null, registration);
  69. }
  70. if (!userDto.isActive()) {
  71. return registerNewUser(dbSession, userDto, registration);
  72. }
  73. return registerExistingUser(dbSession, userDto, registration);
  74. }
  75. }
  76. @CheckForNull
  77. private UserDto getUser(DbSession dbSession, UserIdentity userIdentity, IdentityProvider provider, AuthenticationEvent.Source source) {
  78. // First, try to authenticate using the external ID
  79. UserDto user = dbClient.userDao().selectByExternalIdAndIdentityProvider(dbSession, getProviderIdOrProviderLogin(userIdentity), provider.getKey());
  80. if (user != null) {
  81. return user;
  82. }
  83. // Then, try with the external login, for instance when external ID has changed or is not used by the provider
  84. user = dbClient.userDao().selectByExternalLoginAndIdentityProvider(dbSession, userIdentity.getProviderLogin(), provider.getKey());
  85. if (user == null) {
  86. return null;
  87. }
  88. // all gitlab users have an external ID, so the other two authentication methods should never be used
  89. if (provider.getKey().equals("gitlab")) {
  90. throw loginAlreadyUsedException(userIdentity, source);
  91. }
  92. validateEmailToAvoidLoginRecycling(userIdentity, user, source);
  93. updateUserExternalId(dbSession, user, userIdentity);
  94. return user;
  95. }
  96. private void updateUserExternalId(DbSession dbSession, UserDto user, UserIdentity userIdentity) {
  97. String externalId = userIdentity.getProviderId();
  98. if (externalId != null) {
  99. user.setExternalId(externalId);
  100. dbClient.userDao().update(dbSession, user);
  101. }
  102. }
  103. private static void validateEmailToAvoidLoginRecycling(UserIdentity userIdentity, UserDto user, AuthenticationEvent.Source source) {
  104. String dbEmail = user.getEmail();
  105. if (dbEmail == null) {
  106. return;
  107. }
  108. String externalEmail = userIdentity.getEmail();
  109. if (!dbEmail.equals(externalEmail)) {
  110. LOGGER.warn("User with login '{}' tried to login with email '{}' which doesn't match the email on record '{}'",
  111. userIdentity.getProviderLogin(), externalEmail, dbEmail);
  112. throw loginAlreadyUsedException(userIdentity, source);
  113. }
  114. }
  115. private static AuthenticationException loginAlreadyUsedException(UserIdentity userIdentity, AuthenticationEvent.Source source) {
  116. return authException(
  117. userIdentity,
  118. source,
  119. String.format("Login '%s' is already used", userIdentity.getProviderLogin()),
  120. String.format("You can't sign up because login '%s' is already used by an existing user.", userIdentity.getProviderLogin()));
  121. }
  122. private static AuthenticationException authException(UserIdentity userIdentity, AuthenticationEvent.Source source, String message, String publicMessage) {
  123. return AuthenticationException.newBuilder()
  124. .setSource(source)
  125. .setLogin(userIdentity.getProviderLogin())
  126. .setMessage(message)
  127. .setPublicMessage(publicMessage)
  128. .build();
  129. }
  130. private UserDto registerNewUser(DbSession dbSession, @Nullable UserDto disabledUser, UserRegistration authenticatorParameters) {
  131. Optional<UserDto> otherUserToIndex = detectEmailUpdate(dbSession, authenticatorParameters, disabledUser != null ? disabledUser.getUuid() : null);
  132. NewUser newUser = createNewUser(authenticatorParameters);
  133. if (disabledUser == null) {
  134. return userUpdater.createAndCommit(dbSession, newUser, beforeCommit(dbSession, authenticatorParameters), toArray(otherUserToIndex));
  135. }
  136. return userUpdater.reactivateAndCommit(dbSession, disabledUser, newUser, beforeCommit(dbSession, authenticatorParameters), toArray(otherUserToIndex));
  137. }
  138. private UserDto registerExistingUser(DbSession dbSession, UserDto userDto, UserRegistration authenticatorParameters) {
  139. UpdateUser update = new UpdateUser()
  140. .setEmail(authenticatorParameters.getUserIdentity().getEmail())
  141. .setName(authenticatorParameters.getUserIdentity().getName())
  142. .setExternalIdentity(new ExternalIdentity(
  143. authenticatorParameters.getProvider().getKey(),
  144. authenticatorParameters.getUserIdentity().getProviderLogin(),
  145. authenticatorParameters.getUserIdentity().getProviderId()));
  146. Optional<UserDto> otherUserToIndex = detectEmailUpdate(dbSession, authenticatorParameters, userDto.getUuid());
  147. userUpdater.updateAndCommit(dbSession, userDto, update, beforeCommit(dbSession, authenticatorParameters), toArray(otherUserToIndex));
  148. return userDto;
  149. }
  150. private Consumer<UserDto> beforeCommit(DbSession dbSession, UserRegistration authenticatorParameters) {
  151. return user -> syncGroups(dbSession, authenticatorParameters.getUserIdentity(), user);
  152. }
  153. private Optional<UserDto> detectEmailUpdate(DbSession dbSession, UserRegistration authenticatorParameters, @Nullable String authenticatingUserUuid) {
  154. String email = authenticatorParameters.getUserIdentity().getEmail();
  155. if (email == null) {
  156. return Optional.empty();
  157. }
  158. List<UserDto> existingUsers = dbClient.userDao().selectByEmail(dbSession, email);
  159. if (existingUsers.isEmpty()) {
  160. return Optional.empty();
  161. }
  162. if (existingUsers.size() > 1) {
  163. throw generateExistingEmailError(authenticatorParameters, email);
  164. }
  165. UserDto existingUser = existingUsers.get(0);
  166. if (existingUser == null || existingUser.getUuid().equals(authenticatingUserUuid)) {
  167. return Optional.empty();
  168. }
  169. throw generateExistingEmailError(authenticatorParameters, email);
  170. }
  171. private void syncGroups(DbSession dbSession, UserIdentity userIdentity, UserDto userDto) {
  172. if (!userIdentity.shouldSyncGroups()) {
  173. return;
  174. }
  175. String userLogin = userDto.getLogin();
  176. Set<String> userGroups = new HashSet<>(dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, singletonList(userLogin)).get(userLogin));
  177. Set<String> identityGroups = userIdentity.getGroups();
  178. LOGGER.debug("List of groups returned by the identity provider '{}'", identityGroups);
  179. Collection<String> groupsToAdd = Sets.difference(identityGroups, userGroups);
  180. Collection<String> groupsToRemove = Sets.difference(userGroups, identityGroups);
  181. Collection<String> allGroups = new ArrayList<>(groupsToAdd);
  182. allGroups.addAll(groupsToRemove);
  183. Map<String, GroupDto> groupsByName = dbClient.groupDao().selectByNames(dbSession, allGroups)
  184. .stream()
  185. .collect(uniqueIndex(GroupDto::getName));
  186. addGroups(dbSession, userDto, groupsToAdd, groupsByName);
  187. removeGroups(dbSession, userDto, groupsToRemove, groupsByName);
  188. }
  189. private void addGroups(DbSession dbSession, UserDto userDto, Collection<String> groupsToAdd, Map<String, GroupDto> groupsByName) {
  190. groupsToAdd.stream().map(groupsByName::get).filter(Objects::nonNull).forEach(
  191. groupDto -> {
  192. LOGGER.debug("Adding group '{}' to user '{}'", groupDto.getName(), userDto.getLogin());
  193. dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setGroupUuid(groupDto.getUuid()).setUserUuid(userDto.getUuid()));
  194. });
  195. }
  196. private void removeGroups(DbSession dbSession, UserDto userDto, Collection<String> groupsToRemove, Map<String, GroupDto> groupsByName) {
  197. Optional<GroupDto> defaultGroup = getDefaultGroup(dbSession);
  198. groupsToRemove.stream().map(groupsByName::get)
  199. .filter(Objects::nonNull)
  200. // user should be member of default group only when organizations are disabled, as the IdentityProvider API doesn't handle yet
  201. // organizations
  202. .filter(group -> !defaultGroup.isPresent() || !group.getUuid().equals(defaultGroup.get().getUuid()))
  203. .forEach(groupDto -> {
  204. LOGGER.debug("Removing group '{}' from user '{}'", groupDto.getName(), userDto.getLogin());
  205. dbClient.userGroupDao().delete(dbSession, groupDto.getUuid(), userDto.getUuid());
  206. });
  207. }
  208. private Optional<GroupDto> getDefaultGroup(DbSession dbSession) {
  209. return Optional.of(defaultGroupFinder.findDefaultGroup(dbSession));
  210. }
  211. private static NewUser createNewUser(UserRegistration authenticatorParameters) {
  212. String identityProviderKey = authenticatorParameters.getProvider().getKey();
  213. if (!authenticatorParameters.getProvider().allowsUsersToSignUp()) {
  214. throw AuthenticationException.newBuilder()
  215. .setSource(authenticatorParameters.getSource())
  216. .setLogin(authenticatorParameters.getUserIdentity().getProviderLogin())
  217. .setMessage(format("User signup disabled for provider '%s'", identityProviderKey))
  218. .setPublicMessage(format("'%s' users are not allowed to sign up", identityProviderKey))
  219. .build();
  220. }
  221. String providerLogin = authenticatorParameters.getUserIdentity().getProviderLogin();
  222. return NewUser.builder()
  223. .setLogin(SQ_AUTHORITY.equals(identityProviderKey) ? providerLogin : null)
  224. .setEmail(authenticatorParameters.getUserIdentity().getEmail())
  225. .setName(authenticatorParameters.getUserIdentity().getName())
  226. .setExternalIdentity(
  227. new ExternalIdentity(
  228. identityProviderKey,
  229. providerLogin,
  230. authenticatorParameters.getUserIdentity().getProviderId()))
  231. .build();
  232. }
  233. private static UserDto[] toArray(Optional<UserDto> userDto) {
  234. return userDto.map(u -> new UserDto[] {u}).orElse(new UserDto[] {});
  235. }
  236. private static AuthenticationException generateExistingEmailError(UserRegistration authenticatorParameters, String email) {
  237. return AuthenticationException.newBuilder()
  238. .setSource(authenticatorParameters.getSource())
  239. .setLogin(authenticatorParameters.getUserIdentity().getProviderLogin())
  240. .setMessage(format("Email '%s' is already used", email))
  241. .setPublicMessage(
  242. "This account is already associated with another authentication method. "
  243. + "Sign in using the current authentication method, "
  244. + "or contact your administrator to transfer your account to a different authentication method.")
  245. .build();
  246. }
  247. private static String getProviderIdOrProviderLogin(UserIdentity userIdentity) {
  248. String providerId = userIdentity.getProviderId();
  249. return providerId == null ? userIdentity.getProviderLogin() : providerId;
  250. }
  251. }