James Moger
2012-09-10 fabe060d3a435f116128851f828e35c2af5fde67
commit | author | age
6cca86 1 /*
JM 2  * Copyright 2012 John Crygier
3  * Copyright 2012 gitblit.com
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *     http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 package com.gitblit;
18
19 import java.io.File;
f3b625 20 import java.net.URI;
JC 21 import java.net.URISyntaxException;
22 import java.security.GeneralSecurityException;
3d699c 23 import java.util.List;
6cca86 24
JM 25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
27
28 import com.gitblit.models.TeamModel;
29 import com.gitblit.models.UserModel;
62aeb9 30 import com.gitblit.utils.ArrayUtils;
6cca86 31 import com.gitblit.utils.StringUtils;
f3b625 32 import com.unboundid.ldap.sdk.Attribute;
845b53 33 import com.unboundid.ldap.sdk.ExtendedResult;
f3b625 34 import com.unboundid.ldap.sdk.LDAPConnection;
JC 35 import com.unboundid.ldap.sdk.LDAPException;
36 import com.unboundid.ldap.sdk.LDAPSearchException;
845b53 37 import com.unboundid.ldap.sdk.ResultCode;
f3b625 38 import com.unboundid.ldap.sdk.SearchResult;
JC 39 import com.unboundid.ldap.sdk.SearchResultEntry;
40 import com.unboundid.ldap.sdk.SearchScope;
845b53 41 import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest;
f3b625 42 import com.unboundid.util.ssl.SSLUtil;
JC 43 import com.unboundid.util.ssl.TrustAllTrustManager;
6cca86 44
JM 45 /**
46  * Implementation of an LDAP user service.
47  * 
48  * @author John Crygier
49  */
50 public class LdapUserService extends GitblitUserService {
51
52     public static final Logger logger = LoggerFactory.getLogger(LdapUserService.class);
f3b625 53     
6cca86 54     private IStoredSettings settings;
JM 55
56     public LdapUserService() {
57         super();
58     }
59
60     @Override
61     public void setup(IStoredSettings settings) {
62         this.settings = settings;
668663 63         String file = settings.getString(Keys.realm.ldap.backingUserService, "users.conf");
6cca86 64         File realmFile = GitBlit.getFileOrFolder(file);
JM 65
66         serviceImpl = createUserService(realmFile);
67         logger.info("LDAP User Service backed by " + serviceImpl.toString());
f3b625 68     }
JC 69     
70     private LDAPConnection getLdapConnection() {
71         try {
668663 72             URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server));
JM 73             String bindUserName = settings.getString(Keys.realm.ldap.username, "");
74             String bindPassword = settings.getString(Keys.realm.ldap.password, "");
f3b625 75             int ldapPort = ldapUrl.getPort();
JC 76             
77             if (ldapUrl.getScheme().equalsIgnoreCase("ldaps")) {    // SSL
78                 if (ldapPort == -1)    // Default Port
79                     ldapPort = 636;
80                 
81                 SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager()); 
82                 return new LDAPConnection(sslUtil.createSSLSocketFactory(), ldapUrl.getHost(), ldapPort, bindUserName, bindPassword);
83             } else {
84                 if (ldapPort == -1)    // Default Port
85                     ldapPort = 389;
86                 
845b53 87                 LDAPConnection conn = new LDAPConnection(ldapUrl.getHost(), ldapPort, bindUserName, bindPassword);
SG 88
89                 if (ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
90                     SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
91
92                     ExtendedResult extendedResult = conn.processExtendedOperation(
93                         new StartTLSExtendedRequest(sslUtil.createSSLContext()));
94
95                     if (extendedResult.getResultCode() != ResultCode.SUCCESS) {
96                         throw new LDAPException(extendedResult.getResultCode());
97                     }
98                 }
99                 return conn;
f3b625 100             }
JC 101         } catch (URISyntaxException e) {
845b53 102             logger.error("Bad LDAP URL, should be in the form: ldap(s|+tls)://<server>:<port>", e);
f3b625 103         } catch (GeneralSecurityException e) {
JC 104             logger.error("Unable to create SSL Connection", e);
105         } catch (LDAPException e) {
106             logger.error("Error Connecting to LDAP", e);
107         }
108         
109         return null;
6cca86 110     }
JM 111     
112     /**
113      * Credentials are defined in the LDAP server and can not be manipulated
114      * from Gitblit.
115      *
116      * @return false
117      * @since 1.0.0
118      */
119     @Override
120     public boolean supportsCredentialChanges() {
121         return false;
122     }
123     
124     /**
0aa8cf 125      * If no displayName pattern is defined then Gitblit can manage the display name.
JM 126      *
127      * @return true if Gitblit can manage the user display name
128      * @since 1.0.0
129      */
130     @Override
131     public boolean supportsDisplayNameChanges() {
132         return StringUtils.isEmpty(settings.getString(Keys.realm.ldap.displayName, ""));
133     }
134     
135     /**
136      * If no email pattern is defined then Gitblit can manage the email address.
137      *
138      * @return true if Gitblit can manage the user email address
139      * @since 1.0.0
140      */
141     @Override
142     public boolean supportsEmailAddressChanges() {
143         return StringUtils.isEmpty(settings.getString(Keys.realm.ldap.email, ""));
144     }
145
146     
147     /**
6cca86 148      * If the LDAP server will maintain team memberships then LdapUserService
JM 149      * will not allow team membership changes.  In this scenario all team
150      * changes must be made on the LDAP server by the LDAP administrator.
151      * 
152      * @return true or false
153      * @since 1.0.0
154      */    
155     public boolean supportsTeamMembershipChanges() {
668663 156         return !settings.getBoolean(Keys.realm.ldap.maintainTeams, false);
6cca86 157     }
JM 158
159     @Override
160     public UserModel authenticate(String username, char[] password) {
f3b625 161         String simpleUsername = getSimpleUsername(username);
JC 162         
163         LDAPConnection ldapConnection = getLdapConnection();        
164         if (ldapConnection != null) {
165             // Find the logging in user's DN
668663 166             String accountBase = settings.getString(Keys.realm.ldap.accountBase, "");
JM 167             String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
7e0ce4 168             accountPattern = StringUtils.replace(accountPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
6cca86 169
f3b625 170             SearchResult result = doSearch(ldapConnection, accountBase, accountPattern);
JC 171             if (result != null && result.getEntryCount() == 1) {
172                 SearchResultEntry loggingInUser = result.getSearchEntries().get(0);
173                 String loggingInUserDN = loggingInUser.getDN();
174                 
175                 if (isAuthenticated(ldapConnection, loggingInUserDN, new String(password))) {
049a68 176                     logger.debug("LDAP authenticated: " + username);
f3b625 177                     
JC 178                     UserModel user = getUserModel(simpleUsername);
179                     if (user == null)    // create user object for new authenticated user
7e0ce4 180                         user = new UserModel(simpleUsername);
62aeb9 181
JM 182                     // create a user cookie
183                     if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) {
184                         user.cookie = StringUtils.getSHA1(user.username + new String(password));
185                     }
f3b625 186                     
JC 187                     if (!supportsTeamMembershipChanges())
188                         getTeamsFromLdap(ldapConnection, simpleUsername, loggingInUser, user);
189                     
7e0ce4 190                     // Get User Attributes
JC 191                     setUserAttributes(user, loggingInUser);
f3b625 192
JC 193                     // Push the ldap looked up values to backing file
194                     super.updateUserModel(user);
195                     if (!supportsTeamMembershipChanges()) {
196                         for (TeamModel userTeam : user.teams)
197                             updateTeamModel(userTeam);
6cca86 198                     }
f3b625 199                             
JC 200                     return user;
6cca86 201                 }
JM 202             }
f3b625 203         }
JC 204         
6cca86 205         return null;        
JM 206     }
207
27c74e 208     /**
JM 209      * Set the admin attribute from team memberships retrieved from LDAP.
210      * If we are not storing teams in LDAP and/or we have not defined any
211      * administrator teams, then do not change the admin flag.
212      * 
213      * @param user
214      */
f3b625 215     private void setAdminAttribute(UserModel user) {
27c74e 216         if (!supportsTeamMembershipChanges()) {
JM 217             List<String> admins = settings.getStrings(Keys.realm.ldap.admins);
218             // if we have defined administrative teams, then set admin flag
219             // otherwise leave admin flag unchanged
220             if (!ArrayUtils.isEmpty(admins)) {
221                 user.canAdmin = false;
222                 for (String admin : admins) {
223                     if (admin.startsWith("@")) { // Team
224                         if (user.getTeam(admin.substring(1)) != null)
225                             user.canAdmin = true;
226                     } else
227                         if (user.getName().equalsIgnoreCase(admin))
228                             user.canAdmin = true;
229                 }
230             }
231         }
f3b625 232     }
7e0ce4 233     
JC 234     private void setUserAttributes(UserModel user, SearchResultEntry userEntry) {
235         // Is this user an admin?
236         setAdminAttribute(user);
237         
238         // Don't want visibility into the real password, make up a dummy
239         user.password = "StoredInLDAP";
240         
0aa8cf 241         // Get full name Attribute
JM 242         String displayName = settings.getString(Keys.realm.ldap.displayName, "");        
243         if (!StringUtils.isEmpty(displayName)) {
244             // Replace embedded ${} with attributes
245             if (displayName.contains("${")) {
246                 for (Attribute userAttribute : userEntry.getAttributes())
247                     displayName = StringUtils.replace(displayName, "${" + userAttribute.getName() + "}", userAttribute.getValue());
7e0ce4 248
0aa8cf 249                 user.displayName = displayName;
JM 250             } else {
a01257 251                 Attribute attribute = userEntry.getAttribute(displayName);
JM 252                 if (attribute != null && attribute.hasValue()) {
253                     user.displayName = attribute.getValue();
254                 }
0aa8cf 255             }
7e0ce4 256         }
JC 257         
0aa8cf 258         // Get email address Attribute
JM 259         String email = settings.getString(Keys.realm.ldap.email, "");
260         if (!StringUtils.isEmpty(email)) {
261             if (email.contains("${")) {
262                 for (Attribute userAttribute : userEntry.getAttributes())
263                     email = StringUtils.replace(email, "${" + userAttribute.getName() + "}", userAttribute.getValue());
264
265                 user.emailAddress = email;
266             } else {
a01257 267                 Attribute attribute = userEntry.getAttribute(email);
JM 268                 if (attribute != null && attribute.hasValue()) {
269                     user.emailAddress = attribute.getValue();
270                 }
0aa8cf 271             }
7e0ce4 272         }
JC 273     }
6cca86 274
f3b625 275     private void getTeamsFromLdap(LDAPConnection ldapConnection, String simpleUsername, SearchResultEntry loggingInUser, UserModel user) {
JC 276         String loggingInUserDN = loggingInUser.getDN();
277         
278         user.teams.clear();        // Clear the users team memberships - we're going to get them from LDAP
668663 279         String groupBase = settings.getString(Keys.realm.ldap.groupBase, "");
JM 280         String groupMemberPattern = settings.getString(Keys.realm.ldap.groupMemberPattern, "(&(objectClass=group)(member=${dn}))");
f3b625 281         
7e0ce4 282         groupMemberPattern = StringUtils.replace(groupMemberPattern, "${dn}", escapeLDAPSearchFilter(loggingInUserDN));
JC 283         groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
f3b625 284         
JC 285         // Fill in attributes into groupMemberPattern
286         for (Attribute userAttribute : loggingInUser.getAttributes())
7e0ce4 287             groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", escapeLDAPSearchFilter(userAttribute.getValue()));
f3b625 288         
JC 289         SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, groupMemberPattern);
290         if (teamMembershipResult != null && teamMembershipResult.getEntryCount() > 0) {
291             for (int i = 0; i < teamMembershipResult.getEntryCount(); i++) {
292                 SearchResultEntry teamEntry = teamMembershipResult.getSearchEntries().get(i);
293                 String teamName = teamEntry.getAttribute("cn").getValue();
294                 
295                 TeamModel teamModel = getTeamModel(teamName);
296                 if (teamModel == null)
297                     teamModel = createTeamFromLdap(teamEntry);
298                     
299                 user.teams.add(teamModel);
300                 teamModel.addUser(user.getName());
6cca86 301             }
f3b625 302         }
JC 303     }
304     
305     private TeamModel createTeamFromLdap(SearchResultEntry teamEntry) {
306         TeamModel answer = new TeamModel(teamEntry.getAttributeValue("cn"));
6e15cb 307         // potentially retrieve other attributes here in the future
f3b625 308         
JC 309         return answer;        
310     }
311
312     private SearchResult doSearch(LDAPConnection ldapConnection, String base, String filter) {
313         try {
314             return ldapConnection.search(base, SearchScope.SUB, filter);
315         } catch (LDAPSearchException e) {
316             logger.error("Problem Searching LDAP", e);
317             
6cca86 318             return null;
JM 319         }
320     }
f3b625 321     
JC 322     private boolean isAuthenticated(LDAPConnection ldapConnection, String userDn, String password) {
323         try {
7e0ce4 324             // Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN
f3b625 325             ldapConnection.bind(userDn, password);
JC 326             return true;
327         } catch (LDAPException e) {
049a68 328             logger.error("Error authenticating user", e);
f3b625 329             return false;
JC 330         }
331     }
332
6cca86 333     
JM 334     /**
335      * Returns a simple username without any domain prefixes.
336      * 
337      * @param username
338      * @return a simple username
339      */
340     protected String getSimpleUsername(String username) {
341         int lastSlash = username.lastIndexOf('\\');
342         if (lastSlash > -1) {
343             username = username.substring(lastSlash + 1);
344         }
7e0ce4 345         
6cca86 346         return username;
JM 347     }
7e0ce4 348     
JC 349     // From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
350     public static final String escapeLDAPSearchFilter(String filter) {
351         StringBuilder sb = new StringBuilder();
352         for (int i = 0; i < filter.length(); i++) {
353             char curChar = filter.charAt(i);
354             switch (curChar) {
355             case '\\':
356                 sb.append("\\5c");
357                 break;
358             case '*':
359                 sb.append("\\2a");
360                 break;
361             case '(':
362                 sb.append("\\28");
363                 break;
364             case ')':
365                 sb.append("\\29");
366                 break;
367             case '\u0000': 
368                 sb.append("\\00"); 
369                 break;
370             default:
371                 sb.append(curChar);
372             }
373         }
374         return sb.toString();
375     }
6cca86 376 }