]> source.dussan.org Git - sonarqube.git/blob
be21b9b02b08a13607002fc322b7eb8f9412547e
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2018 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
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;
27 import java.util.Map;
28 import java.util.Objects;
29 import java.util.Optional;
30 import java.util.Set;
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;
56
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;
63
64 public class UserIdentityAuthenticatorImpl implements UserIdentityAuthenticator {
65
66   private static final Logger LOGGER = Loggers.get(UserIdentityAuthenticatorImpl.class);
67
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;
74
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;
83   }
84
85   @Override
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);
91       }
92       if (!userDto.isActive()) {
93         return registerNewUser(dbSession, userDto, authenticatorParameters);
94       }
95       return registerExistingUser(dbSession, userDto, authenticatorParameters);
96     }
97   }
98
99   @CheckForNull
100   private UserDto getUser(DbSession dbSession, UserIdentity userIdentity, IdentityProvider provider) {
101     UserDto user = dbClient.userDao().selectByExternalIdAndIdentityProvider(dbSession, getProviderIdOrProviderLogin(userIdentity), provider.getKey());
102     if (user != null) {
103       return user;
104     }
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();
109     if (login == null) {
110       return null;
111     }
112     return dbClient.userDao().selectByLogin(dbSession, login);
113   }
114
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));
120     }
121     return userUpdater.reactivateAndCommit(dbSession, disabledUser, newUser, u -> syncGroups(dbSession, authenticatorParameters.getUserIdentity(), u), toArray(otherUserToIndex));
122   }
123
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();
133     if (login != null) {
134       update.setLogin(login);
135     }
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));
139     return userDto;
140   }
141
142   private Optional<UserDto> detectEmailUpdate(DbSession dbSession, UserIdentityAuthenticatorParameters authenticatorParameters) {
143     String email = authenticatorParameters.getUserIdentity().getEmail();
144     if (email == null) {
145       return Optional.empty();
146     }
147     List<UserDto> existingUsers = dbClient.userDao().selectByEmail(dbSession, email);
148     if (existingUsers.isEmpty()) {
149       return Optional.empty();
150     }
151     if (existingUsers.size() > 1) {
152       throw generateExistingEmailError(authenticatorParameters, email);
153     }
154
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();
161     }
162     ExistingEmailStrategy existingEmailStrategy = authenticatorParameters.getExistingEmailStrategy();
163     switch (existingEmailStrategy) {
164       case ALLOW:
165         existingUser.setEmail(null);
166         dbClient.userDao().update(dbSession, existingUser);
167         return Optional.of(existingUser);
168       case WARN:
169         throw new EmailAlreadyExistsRedirectionException(email, existingUser, authenticatorParameters.getUserIdentity(), authenticatorParameters.getProvider());
170       case FORBID:
171         throw generateExistingEmailError(authenticatorParameters, email);
172       default:
173         throw new IllegalStateException(format("Unknown strategy %s", existingEmailStrategy));
174     }
175   }
176
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)) {
180       return;
181     }
182     if (!organizationFlags.isEnabled(dbSession)) {
183       return;
184     }
185     String personalOrganizationUuid = user.getOrganizationUuid();
186     if (personalOrganizationUuid == null) {
187       return;
188     }
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) {
194       case ALLOW:
195         organizationUpdater.updateOrganizationKey(dbSession, personalOrganization.get(), requireNonNull(newLogin, "new login cannot be null"));
196         return;
197       case WARN:
198         throw new UpdateLoginRedirectionException(authenticatorParameters.getUserIdentity(), authenticatorParameters.getProvider(), user, personalOrganization.get());
199       default:
200         throw new IllegalStateException(format("Unknown strategy %s", updateLoginStrategy));
201     }
202   }
203
204   private void syncGroups(DbSession dbSession, UserIdentity userIdentity, UserDto userDto) {
205     if (!userIdentity.shouldSyncGroups()) {
206       return;
207     }
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);
212
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)
219       .stream()
220       .collect(uniqueIndex(GroupDto::getName));
221
222     addGroups(dbSession, userDto, groupsToAdd, groupsByName);
223     removeGroups(dbSession, userDto, groupsToRemove, groupsByName);
224   }
225
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(
228       groupDto -> {
229         LOGGER.debug("Adding group '{}' to user '{}'", groupDto.getName(), userDto.getLogin());
230         dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setGroupId(groupDto.getId()).setUserId(userDto.getId()));
231       });
232   }
233
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
239       // organizations
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());
244       });
245   }
246
247   private Optional<GroupDto> getDefaultGroup(DbSession dbSession) {
248     return organizationFlags.isEnabled(dbSession) ? Optional.empty() : Optional.of(defaultGroupFinder.findDefaultGroup(dbSession, defaultOrganizationProvider.get().getUuid()));
249   }
250
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))
259         .build();
260     }
261     return NewUser.builder()
262       .setLogin(authenticatorParameters.getUserIdentity().getLogin())
263       .setEmail(authenticatorParameters.getUserIdentity().getEmail())
264       .setName(authenticatorParameters.getUserIdentity().getName())
265       .setExternalIdentity(
266         new ExternalIdentity(
267           identityProviderKey,
268           authenticatorParameters.getUserIdentity().getProviderLogin(),
269           authenticatorParameters.getUserIdentity().getProviderId()))
270       .build();
271   }
272
273   private static UserDto[] toArray(Optional<UserDto> userDto) {
274     return userDto.map(u -> new UserDto[] {u}).orElse(new UserDto[] {});
275   }
276
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.",
284         email))
285       .build();
286   }
287
288   private static String getProviderIdOrProviderLogin(UserIdentity userIdentity) {
289     String providerId = userIdentity.getProviderId();
290     return providerId == null ? userIdentity.getProviderLogin() : providerId;
291   }
292
293 }