James Moger
2014-07-03 efdb2b3d0c6f03a9aac9e65892cbc8ff755f246f
commit | author | age
04a985 1 /*
JM 2  * Copyright 2013 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.gitblit.manager;
17
18 import java.nio.charset.Charset;
19 import java.security.Principal;
20 import java.text.MessageFormat;
21 import java.util.ArrayList;
22 import java.util.HashMap;
23 import java.util.List;
24 import java.util.Map;
7ab32b 25 import java.util.concurrent.TimeUnit;
04a985 26
JM 27 import javax.servlet.http.Cookie;
28 import javax.servlet.http.HttpServletRequest;
29 import javax.servlet.http.HttpServletResponse;
efdb2b 30 import javax.servlet.http.HttpSession;
04a985 31
JM 32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 import com.gitblit.Constants;
36 import com.gitblit.Constants.AccountType;
37 import com.gitblit.Constants.AuthenticationType;
38 import com.gitblit.IStoredSettings;
39 import com.gitblit.Keys;
40 import com.gitblit.auth.AuthenticationProvider;
41 import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider;
42 import com.gitblit.auth.HtpasswdAuthProvider;
43 import com.gitblit.auth.LdapAuthProvider;
44 import com.gitblit.auth.PAMAuthProvider;
45 import com.gitblit.auth.RedmineAuthProvider;
46 import com.gitblit.auth.SalesforceAuthProvider;
47 import com.gitblit.auth.WindowsAuthProvider;
48 import com.gitblit.models.TeamModel;
49 import com.gitblit.models.UserModel;
bcc8a0 50 import com.gitblit.transport.ssh.SshKey;
04a985 51 import com.gitblit.utils.Base64;
JM 52 import com.gitblit.utils.HttpUtils;
53 import com.gitblit.utils.StringUtils;
54 import com.gitblit.utils.X509Utils.X509Metadata;
55
56 /**
57  * The authentication manager handles user login & logout.
58  *
59  * @author James Moger
60  *
61  */
62 public class AuthenticationManager implements IAuthenticationManager {
63
64     private final Logger logger = LoggerFactory.getLogger(getClass());
65
66     private final IStoredSettings settings;
67
68     private final IRuntimeManager runtimeManager;
69
70     private final IUserManager userManager;
71
72     private final List<AuthenticationProvider> authenticationProviders;
73
74     private final Map<String, Class<? extends AuthenticationProvider>> providerNames;
75
76     private final Map<String, String> legacyRedirects;
77
78     public AuthenticationManager(
79             IRuntimeManager runtimeManager,
80             IUserManager userManager) {
81
82         this.settings = runtimeManager.getSettings();
83         this.runtimeManager = runtimeManager;
84         this.userManager = userManager;
85         this.authenticationProviders = new ArrayList<AuthenticationProvider>();
86
87         // map of shortcut provider names
88         providerNames = new HashMap<String, Class<? extends AuthenticationProvider>>();
89         providerNames.put("htpasswd", HtpasswdAuthProvider.class);
90         providerNames.put("ldap", LdapAuthProvider.class);
91         providerNames.put("pam", PAMAuthProvider.class);
92         providerNames.put("redmine", RedmineAuthProvider.class);
93         providerNames.put("salesforce", SalesforceAuthProvider.class);
94         providerNames.put("windows", WindowsAuthProvider.class);
95
96         // map of legacy external user services
97         legacyRedirects = new HashMap<String, String>();
98         legacyRedirects.put("com.gitblit.HtpasswdUserService", "htpasswd");
99         legacyRedirects.put("com.gitblit.LdapUserService", "ldap");
100         legacyRedirects.put("com.gitblit.PAMUserService", "pam");
101         legacyRedirects.put("com.gitblit.RedmineUserService", "redmine");
102         legacyRedirects.put("com.gitblit.SalesforceUserService", "salesforce");
103         legacyRedirects.put("com.gitblit.WindowsUserService", "windows");
104     }
105
106     @Override
107     public AuthenticationManager start() {
108         // automatically adjust legacy configurations
109         String realm = settings.getString(Keys.realm.userService, "${baseFolder}/users.conf");
110         if (legacyRedirects.containsKey(realm)) {
111             logger.warn("");
088b6f 112             logger.warn(Constants.BORDER2);
04a985 113             logger.warn(" IUserService '{}' is obsolete!", realm);
JM 114             logger.warn(" Please set '{}={}'", "realm.authenticationProviders", legacyRedirects.get(realm));
088b6f 115             logger.warn(Constants.BORDER2);
04a985 116             logger.warn("");
JM 117
118             // conditionally override specified authentication providers
119             if (StringUtils.isEmpty(settings.getString(Keys.realm.authenticationProviders, null))) {
120                 settings.overrideSetting(Keys.realm.authenticationProviders, legacyRedirects.get(realm));
121             }
122         }
123
124         // instantiate and setup specified authentication providers
125         List<String> providers = settings.getStrings(Keys.realm.authenticationProviders);
126         if (providers.isEmpty()) {
127             logger.info("External authentication disabled.");
128         } else {
129             for (String provider : providers) {
130                 try {
131                     Class<?> authClass;
132                     if (providerNames.containsKey(provider)) {
133                         // map the name -> class
134                         authClass = providerNames.get(provider);
135                     } else {
136                         // reflective lookup
137                         authClass = Class.forName(provider);
138                     }
139                     logger.info("setting up {}", authClass.getName());
140                     AuthenticationProvider authImpl = (AuthenticationProvider) authClass.newInstance();
141                     authImpl.setup(runtimeManager, userManager);
142                     authenticationProviders.add(authImpl);
143                 } catch (Exception e) {
144                     logger.error("", e);
145                 }
146             }
147         }
148         return this;
149     }
150
151     @Override
152     public AuthenticationManager stop() {
6659fa 153         for (AuthenticationProvider provider : authenticationProviders) {
JM 154             try {
155                 provider.stop();
156             } catch (Exception e) {
157                 logger.error("Failed to stop " + provider.getClass().getSimpleName(), e);
158             }
159         }
04a985 160         return this;
JM 161     }
bcc8a0 162
b4a63a 163     public void addAuthenticationProvider(AuthenticationProvider prov) {
JM 164         authenticationProviders.add(prov);
165     }
04a985 166
JM 167     /**
168      * Authenticate a user based on HTTP request parameters.
169      *
170      * Authentication by X509Certificate is tried first and then by cookie.
171      *
172      * @param httpRequest
173      * @return a user object or null
174      */
175     @Override
176     public UserModel authenticate(HttpServletRequest httpRequest) {
177         return authenticate(httpRequest, false);
178     }
179
180     /**
181      * Authenticate a user based on HTTP request parameters.
182      *
183      * Authentication by servlet container principal, X509Certificate, cookie,
184      * and finally BASIC header.
185      *
186      * @param httpRequest
187      * @param requiresCertificate
188      * @return a user object or null
189      */
190     @Override
191     public UserModel authenticate(HttpServletRequest httpRequest, boolean requiresCertificate) {
192         // try to authenticate by servlet container principal
193         if (!requiresCertificate) {
194             Principal principal = httpRequest.getUserPrincipal();
195             if (principal != null) {
196                 String username = principal.getName();
197                 if (!StringUtils.isEmpty(username)) {
23e08c 198                     boolean internalAccount = userManager.isInternalAccount(username);
04a985 199                     UserModel user = userManager.getUserModel(username);
JM 200                     if (user != null) {
201                         // existing user
efdb2b 202                         flagSession(httpRequest, AuthenticationType.CONTAINER);
04a985 203                         logger.debug(MessageFormat.format("{0} authenticated by servlet container principal from {1}",
JM 204                                 user.username, httpRequest.getRemoteAddr()));
9aa119 205                         return validateAuthentication(user, AuthenticationType.CONTAINER);
04a985 206                     } else if (settings.getBoolean(Keys.realm.container.autoCreateAccounts, false)
JM 207                             && !internalAccount) {
208                         // auto-create user from an authenticated container principal
209                         user = new UserModel(username.toLowerCase());
210                         user.displayName = username;
211                         user.password = Constants.EXTERNAL_ACCOUNT;
212                         user.accountType = AccountType.CONTAINER;
213                         userManager.updateUserModel(user);
efdb2b 214                         flagSession(httpRequest, AuthenticationType.CONTAINER);
04a985 215                         logger.debug(MessageFormat.format("{0} authenticated and created by servlet container principal from {1}",
JM 216                                 user.username, httpRequest.getRemoteAddr()));
9aa119 217                         return validateAuthentication(user, AuthenticationType.CONTAINER);
04a985 218                     } else if (!internalAccount) {
JM 219                         logger.warn(MessageFormat.format("Failed to find UserModel for {0}, attempted servlet container authentication from {1}",
220                                 principal.getName(), httpRequest.getRemoteAddr()));
221                     }
222                 }
223             }
224         }
225
226         // try to authenticate by certificate
227         boolean checkValidity = settings.getBoolean(Keys.git.enforceCertificateValidity, true);
228         String [] oids = settings.getStrings(Keys.git.certificateUsernameOIDs).toArray(new String[0]);
229         UserModel model = HttpUtils.getUserModelFromCertificate(httpRequest, checkValidity, oids);
230         if (model != null) {
231             // grab real user model and preserve certificate serial number
232             UserModel user = userManager.getUserModel(model.username);
233             X509Metadata metadata = HttpUtils.getCertificateMetadata(httpRequest);
234             if (user != null) {
efdb2b 235                 flagSession(httpRequest, AuthenticationType.CERTIFICATE);
04a985 236                 logger.debug(MessageFormat.format("{0} authenticated by client certificate {1} from {2}",
JM 237                         user.username, metadata.serialNumber, httpRequest.getRemoteAddr()));
9aa119 238                 return validateAuthentication(user, AuthenticationType.CERTIFICATE);
04a985 239             } else {
JM 240                 logger.warn(MessageFormat.format("Failed to find UserModel for {0}, attempted client certificate ({1}) authentication from {2}",
241                         model.username, metadata.serialNumber, httpRequest.getRemoteAddr()));
242             }
243         }
244
245         if (requiresCertificate) {
246             // caller requires client certificate authentication (e.g. git servlet)
247             return null;
248         }
249
7ab32b 250         UserModel user = null;
JM 251
04a985 252         // try to authenticate by cookie
7ab32b 253         String cookie = getCookie(httpRequest);
JM 254         if (!StringUtils.isEmpty(cookie)) {
255             user = userManager.getUserModel(cookie.toCharArray());
256             if (user != null) {
efdb2b 257                 flagSession(httpRequest, AuthenticationType.COOKIE);
7ab32b 258                 logger.debug(MessageFormat.format("{0} authenticated by cookie from {1}",
04a985 259                     user.username, httpRequest.getRemoteAddr()));
9aa119 260                 return validateAuthentication(user, AuthenticationType.COOKIE);
7ab32b 261             }
04a985 262         }
JM 263
264         // try to authenticate by BASIC
265         final String authorization = httpRequest.getHeader("Authorization");
266         if (authorization != null && authorization.startsWith("Basic")) {
267             // Authorization: Basic base64credentials
268             String base64Credentials = authorization.substring("Basic".length()).trim();
269             String credentials = new String(Base64.decode(base64Credentials),
270                     Charset.forName("UTF-8"));
271             // credentials = username:password
272             final String[] values = credentials.split(":", 2);
273
274             if (values.length == 2) {
275                 String username = values[0];
276                 char[] password = values[1].toCharArray();
277                 user = authenticate(username, password);
278                 if (user != null) {
efdb2b 279                     flagSession(httpRequest, AuthenticationType.CREDENTIALS);
04a985 280                     logger.debug(MessageFormat.format("{0} authenticated by BASIC request header from {1}",
JM 281                             user.username, httpRequest.getRemoteAddr()));
9aa119 282                     return validateAuthentication(user, AuthenticationType.CREDENTIALS);
04a985 283                 } else {
JM 284                     logger.warn(MessageFormat.format("Failed login attempt for {0}, invalid credentials from {1}",
285                             username, httpRequest.getRemoteAddr()));
286                 }
287             }
288         }
289         return null;
9aa119 290     }
JM 291
292     /**
44e2ee 293      * Authenticate a user based on a public key.
e3b636 294      *
44e2ee 295      * This implementation assumes that the authentication has already take place
JM 296      * (e.g. SSHDaemon) and that this is a validation/verification of the user.
297      *
298      * @param username
299      * @param key
e3b636 300      * @return a user object or null
DO 301      */
302     @Override
bcc8a0 303     public UserModel authenticate(String username, SshKey key) {
e3b636 304         if (username != null) {
DO 305             if (!StringUtils.isEmpty(username)) {
306                 UserModel user = userManager.getUserModel(username);
307                 if (user != null) {
308                     // existing user
44e2ee 309                     logger.debug(MessageFormat.format("{0} authenticated by {1} public key",
JM 310                             user.username, key.getAlgorithm()));
311                     return validateAuthentication(user, AuthenticationType.PUBLIC_KEY);
e3b636 312                 }
44e2ee 313                 logger.warn(MessageFormat.format("Failed to find UserModel for {0} during public key authentication",
JM 314                             username));
e3b636 315             }
DO 316         } else {
44e2ee 317             logger.warn("Empty user passed to AuthenticationManager.authenticate!");
e3b636 318         }
DO 319         return null;
320     }
321
322
323     /**
9aa119 324      * This method allows the authentication manager to reject authentication
JM 325      * attempts.  It is called after the username/secret have been verified to
326      * ensure that the authentication technique has been logged.
327      *
328      * @param user
329      * @return
330      */
331     protected UserModel validateAuthentication(UserModel user, AuthenticationType type) {
332         if (user == null) {
333             return null;
334         }
335         if (user.disabled) {
336             // user has been disabled
337             logger.warn("Rejected {} authentication attempt by disabled account \"{}\"",
338                     type, user.username);
339             return null;
340         }
341         return user;
04a985 342     }
JM 343
efdb2b 344     protected void flagSession(HttpServletRequest httpRequest, AuthenticationType authenticationType) {
JM 345         httpRequest.getSession().setAttribute(Constants.AUTHENTICATION_TYPE, authenticationType);
04a985 346     }
JM 347
348     /**
349      * Authenticate a user based on a username and password.
350      *
351      * @see IUserService.authenticate(String, char[])
352      * @param username
353      * @param password
354      * @return a user object or null
355      */
356     @Override
357     public UserModel authenticate(String username, char[] password) {
358         if (StringUtils.isEmpty(username)) {
359             // can not authenticate empty username
360             return null;
361         }
362
363         String usernameDecoded = StringUtils.decodeUsername(username);
364         String pw = new String(password);
365         if (StringUtils.isEmpty(pw)) {
366             // can not authenticate empty password
367             return null;
368         }
369
370         UserModel user = userManager.getUserModel(usernameDecoded);
1293c2 371
JM 372         // try local authentication
373         if (user != null && user.isLocalAccount()) {
b4a63a 374             return authenticateLocal(user, password);
04a985 375         }
JM 376
377         // try registered external authentication providers
b4a63a 378         for (AuthenticationProvider provider : authenticationProviders) {
JM 379             if (provider instanceof UsernamePasswordAuthenticationProvider) {
380                 UserModel returnedUser = provider.authenticate(usernameDecoded, password);
381                 if (returnedUser != null) {
382                     // user authenticated
383                     returnedUser.accountType = provider.getAccountType();
384                     return validateAuthentication(returnedUser, AuthenticationType.CREDENTIALS);
04a985 385                 }
JM 386             }
387         }
bcc8a0 388
b4a63a 389         // could not authenticate locally or with a provider
JM 390         return null;
391     }
bcc8a0 392
b4a63a 393     /**
JM 394      * Returns a UserModel if local authentication succeeds.
bcc8a0 395      *
b4a63a 396      * @param user
JM 397      * @param password
398      * @return a UserModel if local authentication succeeds, null otherwise
399      */
400     protected UserModel authenticateLocal(UserModel user, char [] password) {
401         UserModel returnedUser = null;
402         if (user.password.startsWith(StringUtils.MD5_TYPE)) {
403             // password digest
404             String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password));
405             if (user.password.equalsIgnoreCase(md5)) {
406                 returnedUser = user;
407             }
408         } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) {
409             // username+password digest
410             String md5 = StringUtils.COMBINED_MD5_TYPE
411                     + StringUtils.getMD5(user.username.toLowerCase() + new String(password));
412             if (user.password.equalsIgnoreCase(md5)) {
413                 returnedUser = user;
414             }
415         } else if (user.password.equals(new String(password))) {
416             // plain-text password
417             returnedUser = user;
418         }
419         return validateAuthentication(returnedUser, AuthenticationType.CREDENTIALS);
04a985 420     }
JM 421
422     /**
7ab32b 423      * Returns the Gitlbit cookie in the request.
JM 424      *
425      * @param request
426      * @return the Gitblit cookie for the request or null if not found
427      */
428     @Override
429     public String getCookie(HttpServletRequest request) {
430         if (settings.getBoolean(Keys.web.allowCookieAuthentication, true)) {
431             Cookie[] cookies = request.getCookies();
432             if (cookies != null && cookies.length > 0) {
433                 for (Cookie cookie : cookies) {
434                     if (cookie.getName().equals(Constants.NAME)) {
435                         String value = cookie.getValue();
436                         return value;
437                     }
438                 }
439             }
440         }
441         return null;
442     }
443
444     /**
04a985 445      * Sets a cookie for the specified user.
JM 446      *
447      * @param response
448      * @param user
449      */
450     @Override
ec7ed8 451     @Deprecated
04a985 452     public void setCookie(HttpServletResponse response, UserModel user) {
ec7ed8 453         setCookie(null, response, user);
JM 454     }
455
456     /**
457      * Sets a cookie for the specified user.
458      *
459      * @param request
460      * @param response
461      * @param user
462      */
463     @Override
464     public void setCookie(HttpServletRequest request, HttpServletResponse response, UserModel user) {
04a985 465         if (settings.getBoolean(Keys.web.allowCookieAuthentication, true)) {
efdb2b 466             HttpSession session = request.getSession();
JM 467             AuthenticationType authenticationType = (AuthenticationType) session.getAttribute(Constants.AUTHENTICATION_TYPE);
468             boolean standardLogin = authenticationType.isStandard();
04a985 469
JM 470             if (standardLogin) {
471                 Cookie userCookie;
472                 if (user == null) {
473                     // clear cookie for logout
474                     userCookie = new Cookie(Constants.NAME, "");
475                 } else {
476                     // set cookie for login
477                     String cookie = userManager.getCookie(user);
478                     if (StringUtils.isEmpty(cookie)) {
479                         // create empty cookie
480                         userCookie = new Cookie(Constants.NAME, "");
481                     } else {
482                         // create real cookie
483                         userCookie = new Cookie(Constants.NAME, cookie);
7ab32b 484                         // expire the cookie in 7 days
JM 485                         userCookie.setMaxAge((int) TimeUnit.DAYS.toSeconds(7));
04a985 486                     }
JM 487                 }
ec7ed8 488                 String path = "/";
JM 489                 if (request != null) {
490                     if (!StringUtils.isEmpty(request.getContextPath())) {
491                         path = request.getContextPath();
492                     }
493                 }
494                 userCookie.setPath(path);
04a985 495                 response.addCookie(userCookie);
JM 496             }
497         }
498     }
499
500     /**
501      * Logout a user.
502      *
ec7ed8 503      * @param response
04a985 504      * @param user
JM 505      */
506     @Override
ec7ed8 507     @Deprecated
04a985 508     public void logout(HttpServletResponse response, UserModel user) {
ec7ed8 509         setCookie(null, response,  null);
JM 510     }
511
512     /**
513      * Logout a user.
514      *
515      * @param request
516      * @param response
517      * @param user
518      */
519     @Override
520     public void logout(HttpServletRequest request, HttpServletResponse response, UserModel user) {
521         setCookie(request, response,  null);
04a985 522     }
JM 523
524     /**
525      * Returns true if the user's credentials can be changed.
526      *
527      * @param user
528      * @return true if the user service supports credential changes
529      */
530     @Override
531     public boolean supportsCredentialChanges(UserModel user) {
532         return (user != null && user.isLocalAccount()) || findProvider(user).supportsCredentialChanges();
533     }
534
535     /**
536      * Returns true if the user's display name can be changed.
537      *
538      * @param user
539      * @return true if the user service supports display name changes
540      */
541     @Override
542     public boolean supportsDisplayNameChanges(UserModel user) {
543         return (user != null && user.isLocalAccount()) || findProvider(user).supportsDisplayNameChanges();
544     }
545
546     /**
547      * Returns true if the user's email address can be changed.
548      *
549      * @param user
550      * @return true if the user service supports email address changes
551      */
552     @Override
553     public boolean supportsEmailAddressChanges(UserModel user) {
554         return (user != null && user.isLocalAccount()) || findProvider(user).supportsEmailAddressChanges();
555     }
556
557     /**
558      * Returns true if the user's team memberships can be changed.
559      *
560      * @param user
561      * @return true if the user service supports team membership changes
562      */
563     @Override
564     public boolean supportsTeamMembershipChanges(UserModel user) {
565         return (user != null && user.isLocalAccount()) || findProvider(user).supportsTeamMembershipChanges();
566     }
567
568     /**
569      * Returns true if the team memberships can be changed.
570      *
571      * @param user
572      * @return true if the team membership can be changed
573      */
574     @Override
575     public boolean supportsTeamMembershipChanges(TeamModel team) {
576         return (team != null && team.isLocalTeam()) || findProvider(team).supportsTeamMembershipChanges();
577     }
578
579     protected AuthenticationProvider findProvider(UserModel user) {
580         for (AuthenticationProvider provider : authenticationProviders) {
581             if (provider.getAccountType().equals(user.accountType)) {
582                 return provider;
583             }
584         }
585         return AuthenticationProvider.NULL_PROVIDER;
586     }
587
588     protected AuthenticationProvider findProvider(TeamModel team) {
589         for (AuthenticationProvider provider : authenticationProviders) {
590             if (provider.getAccountType().equals(team.accountType)) {
591                 return provider;
592             }
593         }
594         return AuthenticationProvider.NULL_PROVIDER;
595     }
596 }