3 * Copyright (C) 2009-2018 SonarSource SA
4 * mailto:info AT sonarsource DOT com
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.
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.
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.
20 package org.sonar.server.authentication;
22 import com.google.common.collect.Sets;
23 import java.util.ArrayList;
24 import java.util.Collection;
25 import java.util.HashSet;
26 import java.util.List;
28 import java.util.Objects;
29 import java.util.Optional;
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.organization.OrganizationDto;
40 import org.sonar.db.user.GroupDto;
41 import org.sonar.db.user.UserDto;
42 import org.sonar.db.user.UserGroupDto;
43 import org.sonar.server.authentication.UserIdentityAuthenticatorParameters.ExistingEmailStrategy;
44 import org.sonar.server.authentication.event.AuthenticationException;
45 import org.sonar.server.authentication.exception.EmailAlreadyExistsRedirectionException;
46 import org.sonar.server.authentication.exception.UpdateLoginRedirectionException;
47 import org.sonar.server.organization.DefaultOrganization;
48 import org.sonar.server.organization.DefaultOrganizationProvider;
49 import org.sonar.server.organization.OrganizationFlags;
50 import org.sonar.server.organization.OrganizationUpdater;
51 import org.sonar.server.user.ExternalIdentity;
52 import org.sonar.server.user.NewUser;
53 import org.sonar.server.user.UpdateUser;
54 import org.sonar.server.user.UserUpdater;
55 import org.sonar.server.usergroups.DefaultGroupFinder;
57 import static com.google.common.base.Preconditions.checkState;
58 import static java.lang.String.format;
59 import static java.util.Collections.singletonList;
60 import static java.util.Objects.requireNonNull;
61 import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
62 import static org.sonar.server.authentication.UserIdentityAuthenticatorParameters.UpdateLoginStrategy;
64 public class UserIdentityAuthenticatorImpl implements UserIdentityAuthenticator {
66 private static final Logger LOGGER = Loggers.get(UserIdentityAuthenticatorImpl.class);
68 private final DbClient dbClient;
69 private final UserUpdater userUpdater;
70 private final DefaultOrganizationProvider defaultOrganizationProvider;
71 private final OrganizationFlags organizationFlags;
72 private final OrganizationUpdater organizationUpdater;
73 private final DefaultGroupFinder defaultGroupFinder;
75 public UserIdentityAuthenticatorImpl(DbClient dbClient, UserUpdater userUpdater, DefaultOrganizationProvider defaultOrganizationProvider, OrganizationFlags organizationFlags,
76 OrganizationUpdater organizationUpdater, DefaultGroupFinder defaultGroupFinder) {
77 this.dbClient = dbClient;
78 this.userUpdater = userUpdater;
79 this.defaultOrganizationProvider = defaultOrganizationProvider;
80 this.organizationFlags = organizationFlags;
81 this.organizationUpdater = organizationUpdater;
82 this.defaultGroupFinder = defaultGroupFinder;
86 public UserDto authenticate(UserIdentityAuthenticatorParameters authenticatorParameters) {
87 try (DbSession dbSession = dbClient.openSession(false)) {
88 UserDto userDto = getUser(dbSession, authenticatorParameters.getUserIdentity(), authenticatorParameters.getProvider());
89 if (userDto == null) {
90 return registerNewUser(dbSession, null, authenticatorParameters);
92 if (!userDto.isActive()) {
93 return registerNewUser(dbSession, userDto, authenticatorParameters);
95 return registerExistingUser(dbSession, userDto, authenticatorParameters);
100 private UserDto getUser(DbSession dbSession, UserIdentity userIdentity, IdentityProvider provider) {
101 UserDto user = dbClient.userDao().selectByExternalIdAndIdentityProvider(dbSession, getProviderIdOrProviderLogin(userIdentity), provider.getKey());
105 // We need to search by login because :
106 // 1. user may have been provisioned,
107 // 2. user may have been disabled.
108 String login = userIdentity.getLogin();
112 return dbClient.userDao().selectByLogin(dbSession, login);
115 private UserDto registerNewUser(DbSession dbSession, @Nullable UserDto disabledUser, UserIdentityAuthenticatorParameters authenticatorParameters) {
116 Optional<UserDto> otherUserToIndex = detectEmailUpdate(dbSession, authenticatorParameters);
117 NewUser newUser = createNewUser(authenticatorParameters);
118 if (disabledUser == null) {
119 return userUpdater.createAndCommit(dbSession, newUser, u -> syncGroups(dbSession, authenticatorParameters.getUserIdentity(), u), toArray(otherUserToIndex));
121 return userUpdater.reactivateAndCommit(dbSession, disabledUser, newUser, u -> syncGroups(dbSession, authenticatorParameters.getUserIdentity(), u), toArray(otherUserToIndex));
124 private UserDto registerExistingUser(DbSession dbSession, UserDto userDto, UserIdentityAuthenticatorParameters authenticatorParameters) {
125 UpdateUser update = new UpdateUser()
126 .setEmail(authenticatorParameters.getUserIdentity().getEmail())
127 .setName(authenticatorParameters.getUserIdentity().getName())
128 .setExternalIdentity(new ExternalIdentity(
129 authenticatorParameters.getProvider().getKey(),
130 authenticatorParameters.getUserIdentity().getProviderLogin(),
131 authenticatorParameters.getUserIdentity().getProviderId()));
132 String login = authenticatorParameters.getUserIdentity().getLogin();
134 update.setLogin(login);
136 detectLoginUpdate(dbSession, userDto, update, authenticatorParameters);
137 Optional<UserDto> otherUserToIndex = detectEmailUpdate(dbSession, authenticatorParameters);
138 userUpdater.updateAndCommit(dbSession, userDto, update, u -> syncGroups(dbSession, authenticatorParameters.getUserIdentity(), u), toArray(otherUserToIndex));
142 private Optional<UserDto> detectEmailUpdate(DbSession dbSession, UserIdentityAuthenticatorParameters authenticatorParameters) {
143 String email = authenticatorParameters.getUserIdentity().getEmail();
145 return Optional.empty();
147 List<UserDto> existingUsers = dbClient.userDao().selectByEmail(dbSession, email);
148 if (existingUsers.isEmpty()) {
149 return Optional.empty();
151 if (existingUsers.size() > 1) {
152 throw generateExistingEmailError(authenticatorParameters, email);
155 UserDto existingUser = existingUsers.get(0);
156 if (existingUser == null
157 || Objects.equals(existingUser.getLogin(), authenticatorParameters.getUserIdentity().getLogin())
158 || (Objects.equals(existingUser.getExternalId(), getProviderIdOrProviderLogin(authenticatorParameters.getUserIdentity()))
159 && Objects.equals(existingUser.getExternalIdentityProvider(), authenticatorParameters.getProvider().getKey()))) {
160 return Optional.empty();
162 ExistingEmailStrategy existingEmailStrategy = authenticatorParameters.getExistingEmailStrategy();
163 switch (existingEmailStrategy) {
165 existingUser.setEmail(null);
166 dbClient.userDao().update(dbSession, existingUser);
167 return Optional.of(existingUser);
169 throw new EmailAlreadyExistsRedirectionException(email, existingUser, authenticatorParameters.getUserIdentity(), authenticatorParameters.getProvider());
171 throw generateExistingEmailError(authenticatorParameters, email);
173 throw new IllegalStateException(format("Unknown strategy %s", existingEmailStrategy));
177 private void detectLoginUpdate(DbSession dbSession, UserDto user, UpdateUser update, UserIdentityAuthenticatorParameters authenticatorParameters) {
178 String newLogin = update.login();
179 if (!update.isLoginChanged() || user.getLogin().equals(newLogin)) {
182 if (!organizationFlags.isEnabled(dbSession)) {
185 String personalOrganizationUuid = user.getOrganizationUuid();
186 if (personalOrganizationUuid == null) {
189 Optional<OrganizationDto> personalOrganization = dbClient.organizationDao().selectByUuid(dbSession, personalOrganizationUuid);
190 checkState(personalOrganization.isPresent(),
191 "Cannot find personal organization uuid '%s' for user '%s'", personalOrganizationUuid, user.getLogin());
192 UpdateLoginStrategy updateLoginStrategy = authenticatorParameters.getUpdateLoginStrategy();
193 switch (updateLoginStrategy) {
195 organizationUpdater.updateOrganizationKey(dbSession, personalOrganization.get(), requireNonNull(newLogin, "new login cannot be null"));
198 throw new UpdateLoginRedirectionException(authenticatorParameters.getUserIdentity(), authenticatorParameters.getProvider(), user, personalOrganization.get());
200 throw new IllegalStateException(format("Unknown strategy %s", updateLoginStrategy));
204 private void syncGroups(DbSession dbSession, UserIdentity userIdentity, UserDto userDto) {
205 if (!userIdentity.shouldSyncGroups()) {
208 String userLogin = userDto.getLogin();
209 Set<String> userGroups = new HashSet<>(dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, singletonList(userLogin)).get(userLogin));
210 Set<String> identityGroups = userIdentity.getGroups();
211 LOGGER.debug("List of groups returned by the identity provider '{}'", identityGroups);
213 Collection<String> groupsToAdd = Sets.difference(identityGroups, userGroups);
214 Collection<String> groupsToRemove = Sets.difference(userGroups, identityGroups);
215 Collection<String> allGroups = new ArrayList<>(groupsToAdd);
216 allGroups.addAll(groupsToRemove);
217 DefaultOrganization defaultOrganization = defaultOrganizationProvider.get();
218 Map<String, GroupDto> groupsByName = dbClient.groupDao().selectByNames(dbSession, defaultOrganization.getUuid(), allGroups)
220 .collect(uniqueIndex(GroupDto::getName));
222 addGroups(dbSession, userDto, groupsToAdd, groupsByName);
223 removeGroups(dbSession, userDto, groupsToRemove, groupsByName);
226 private void addGroups(DbSession dbSession, UserDto userDto, Collection<String> groupsToAdd, Map<String, GroupDto> groupsByName) {
227 groupsToAdd.stream().map(groupsByName::get).filter(Objects::nonNull).forEach(
229 LOGGER.debug("Adding group '{}' to user '{}'", groupDto.getName(), userDto.getLogin());
230 dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setGroupId(groupDto.getId()).setUserId(userDto.getId()));
234 private void removeGroups(DbSession dbSession, UserDto userDto, Collection<String> groupsToRemove, Map<String, GroupDto> groupsByName) {
235 Optional<GroupDto> defaultGroup = getDefaultGroup(dbSession);
236 groupsToRemove.stream().map(groupsByName::get)
237 .filter(Objects::nonNull)
238 // user should be member of default group only when organizations are disabled, as the IdentityProvider API doesn't handle yet
240 .filter(group -> !defaultGroup.isPresent() || !group.getId().equals(defaultGroup.get().getId()))
241 .forEach(groupDto -> {
242 LOGGER.debug("Removing group '{}' from user '{}'", groupDto.getName(), userDto.getLogin());
243 dbClient.userGroupDao().delete(dbSession, groupDto.getId(), userDto.getId());
247 private Optional<GroupDto> getDefaultGroup(DbSession dbSession) {
248 return organizationFlags.isEnabled(dbSession) ? Optional.empty() : Optional.of(defaultGroupFinder.findDefaultGroup(dbSession, defaultOrganizationProvider.get().getUuid()));
251 private static NewUser createNewUser(UserIdentityAuthenticatorParameters authenticatorParameters) {
252 String identityProviderKey = authenticatorParameters.getProvider().getKey();
253 if (!authenticatorParameters.getProvider().allowsUsersToSignUp()) {
254 throw AuthenticationException.newBuilder()
255 .setSource(authenticatorParameters.getSource())
256 .setLogin(authenticatorParameters.getUserIdentity().getProviderLogin())
257 .setMessage(format("User signup disabled for provider '%s'", identityProviderKey))
258 .setPublicMessage(format("'%s' users are not allowed to sign up", identityProviderKey))
261 return NewUser.builder()
262 .setLogin(authenticatorParameters.getUserIdentity().getLogin())
263 .setEmail(authenticatorParameters.getUserIdentity().getEmail())
264 .setName(authenticatorParameters.getUserIdentity().getName())
265 .setExternalIdentity(
266 new ExternalIdentity(
268 authenticatorParameters.getUserIdentity().getProviderLogin(),
269 authenticatorParameters.getUserIdentity().getProviderId()))
273 private static UserDto[] toArray(Optional<UserDto> userDto) {
274 return userDto.map(u -> new UserDto[] {u}).orElse(new UserDto[] {});
277 private static AuthenticationException generateExistingEmailError(UserIdentityAuthenticatorParameters authenticatorParameters, String email) {
278 return AuthenticationException.newBuilder()
279 .setSource(authenticatorParameters.getSource())
280 .setLogin(authenticatorParameters.getUserIdentity().getProviderLogin())
281 .setMessage(format("Email '%s' is already used", email))
282 .setPublicMessage(format(
283 "You can't sign up because email '%s' is already used by an existing user. This means that you probably already registered with another account.",
288 private static String getProviderIdOrProviderLogin(UserIdentity userIdentity) {
289 String providerId = userIdentity.getProviderId();
290 return providerId == null ? userIdentity.getProviderLogin() : providerId;