]> source.dussan.org Git - sonarqube.git/blob
85aa225f74611078de03d7e30f0c187a0e862380
[sonarqube.git] /
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
22 import java.util.Collection;
23 import java.util.HashSet;
24 import java.util.Locale;
25 import java.util.Optional;
26 import javax.servlet.http.HttpServletRequest;
27 import org.sonar.api.Startable;
28 import org.sonar.api.config.Configuration;
29 import org.sonar.api.security.Authenticator;
30 import org.sonar.api.security.ExternalGroupsProvider;
31 import org.sonar.api.security.ExternalUsersProvider;
32 import org.sonar.api.security.SecurityRealm;
33 import org.sonar.api.security.UserDetails;
34 import org.sonar.api.server.authentication.Display;
35 import org.sonar.api.server.authentication.IdentityProvider;
36 import org.sonar.api.server.authentication.UserIdentity;
37 import org.sonar.api.utils.log.Logger;
38 import org.sonar.api.utils.log.Loggers;
39 import org.sonar.db.user.UserDto;
40 import org.sonar.server.authentication.UserRegistration.ExistingEmailStrategy;
41 import org.sonar.server.authentication.event.AuthenticationEvent;
42 import org.sonar.server.authentication.event.AuthenticationEvent.Source;
43 import org.sonar.server.authentication.event.AuthenticationException;
44 import org.sonar.server.user.SecurityRealmFactory;
45
46 import static java.util.Objects.requireNonNull;
47 import static org.apache.commons.lang.StringUtils.isEmpty;
48 import static org.apache.commons.lang.StringUtils.trimToNull;
49 import static org.sonar.server.user.ExternalIdentity.SQ_AUTHORITY;
50
51 /**
52  * Delegates the validation of credentials to an external system, e.g. LDAP.
53  */
54 public class CredentialsExternalAuthentication implements Startable {
55
56   private static final Logger LOG = Loggers.get(CredentialsExternalAuthentication.class);
57
58   private final Configuration config;
59   private final SecurityRealmFactory securityRealmFactory;
60   private final UserRegistrar userRegistrar;
61   private final AuthenticationEvent authenticationEvent;
62
63   private SecurityRealm realm;
64   private Authenticator authenticator;
65   private ExternalUsersProvider externalUsersProvider;
66   private ExternalGroupsProvider externalGroupsProvider;
67
68   public CredentialsExternalAuthentication(Configuration config, SecurityRealmFactory securityRealmFactory,
69     UserRegistrar userRegistrar, AuthenticationEvent authenticationEvent) {
70     this.config = config;
71     this.securityRealmFactory = securityRealmFactory;
72     this.userRegistrar = userRegistrar;
73     this.authenticationEvent = authenticationEvent;
74   }
75
76   @Override
77   public void start() {
78     realm = securityRealmFactory.getRealm();
79     if (realm != null) {
80       authenticator = requireNonNull(realm.doGetAuthenticator(), "No authenticator available");
81       externalUsersProvider = requireNonNull(realm.getUsersProvider(), "No users provider available");
82       externalGroupsProvider = realm.getGroupsProvider();
83     }
84   }
85
86   public Optional<UserDto> authenticate(Credentials credentials, HttpServletRequest request, AuthenticationEvent.Method method) {
87     if (realm == null) {
88       return Optional.empty();
89     }
90     return Optional.of(doAuthenticate(fixCase(credentials), request, method));
91   }
92
93   private UserDto doAuthenticate(Credentials credentials, HttpServletRequest request, AuthenticationEvent.Method method) {
94     try {
95       ExternalUsersProvider.Context externalUsersProviderContext = new ExternalUsersProvider.Context(credentials.getLogin(), request);
96       UserDetails details = externalUsersProvider.doGetUserDetails(externalUsersProviderContext);
97       if (details == null) {
98         throw AuthenticationException.newBuilder()
99           .setSource(realmEventSource(method))
100           .setLogin(credentials.getLogin())
101           .setMessage("No user details")
102           .build();
103       }
104       Authenticator.Context authenticatorContext = new Authenticator.Context(credentials.getLogin(), credentials.getPassword().orElse(null), request);
105       boolean status = authenticator.doAuthenticate(authenticatorContext);
106       if (!status) {
107         throw AuthenticationException.newBuilder()
108           .setSource(realmEventSource(method))
109           .setLogin(credentials.getLogin())
110           .setMessage("Realm returned authenticate=false")
111           .build();
112       }
113       UserDto userDto = synchronize(credentials.getLogin(), details, request, method);
114       authenticationEvent.loginSuccess(request, credentials.getLogin(), realmEventSource(method));
115       return userDto;
116     } catch (AuthenticationException e) {
117       throw e;
118     } catch (Exception e) {
119       // It seems that with Realm API it's expected to log the error and to not authenticate the user
120       LOG.error("Error during authentication", e);
121       throw AuthenticationException.newBuilder()
122         .setSource(realmEventSource(method))
123         .setLogin(credentials.getLogin())
124         .setMessage(e.getMessage())
125         .build();
126     }
127   }
128
129   private Source realmEventSource(AuthenticationEvent.Method method) {
130     return Source.realm(method, realm.getName());
131   }
132
133   private UserDto synchronize(String userLogin, UserDetails details, HttpServletRequest request, AuthenticationEvent.Method method) {
134     String name = details.getName();
135     UserIdentity.Builder userIdentityBuilder = UserIdentity.builder()
136       .setName(isEmpty(name) ? userLogin : name)
137       .setEmail(trimToNull(details.getEmail()))
138       .setProviderLogin(userLogin);
139     if (externalGroupsProvider != null) {
140       ExternalGroupsProvider.Context context = new ExternalGroupsProvider.Context(userLogin, request);
141       Collection<String> groups = externalGroupsProvider.doGetGroups(context);
142       userIdentityBuilder.setGroups(new HashSet<>(groups));
143     }
144     return userRegistrar.register(
145       UserRegistration.builder()
146         .setUserIdentity(userIdentityBuilder.build())
147         .setProvider(new ExternalIdentityProvider())
148         .setSource(realmEventSource(method))
149         .setExistingEmailStrategy(ExistingEmailStrategy.FORBID)
150         .build());
151   }
152
153   private Credentials fixCase(Credentials credentials) {
154     if (config.getBoolean("sonar.authenticator.downcase").orElse(false)) {
155       return new Credentials(credentials.getLogin().toLowerCase(Locale.ENGLISH), credentials.getPassword().orElse(null));
156     }
157     return credentials;
158   }
159
160   private static class ExternalIdentityProvider implements IdentityProvider {
161     @Override
162     public String getKey() {
163       return SQ_AUTHORITY;
164     }
165
166     @Override
167     public String getName() {
168       return SQ_AUTHORITY;
169     }
170
171     @Override
172     public Display getDisplay() {
173       return null;
174     }
175
176     @Override
177     public boolean isEnabled() {
178       return true;
179     }
180
181     @Override
182     public boolean allowsUsersToSignUp() {
183       return true;
184     }
185   }
186
187   @Override
188   public void stop() {
189     // Nothing to do
190   }
191 }