James Moger
2012-10-22 eba89539a29deba954035056437279088c3e047b
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         
301adb 163         LDAPConnection ldapConnection = getLdapConnection();
f3b625 164         if (ldapConnection != null) {
301adb 165             try {
JM 166                 // Find the logging in user's DN
167                 String accountBase = settings.getString(Keys.realm.ldap.accountBase, "");
168                 String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
169                 accountPattern = StringUtils.replace(accountPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
6cca86 170
301adb 171                 SearchResult result = doSearch(ldapConnection, accountBase, accountPattern);
JM 172                 if (result != null && result.getEntryCount() == 1) {
173                     SearchResultEntry loggingInUser = result.getSearchEntries().get(0);
174                     String loggingInUserDN = loggingInUser.getDN();
62aeb9 175
301adb 176                     if (isAuthenticated(ldapConnection, loggingInUserDN, new String(password))) {
JM 177                         logger.debug("LDAP authenticated: " + username);
178
179                         UserModel user = getUserModel(simpleUsername);
180                         if (user == null)    // create user object for new authenticated user
181                             user = new UserModel(simpleUsername);
182
183                         // create a user cookie
184                         if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) {
185                             user.cookie = StringUtils.getSHA1(user.username + new String(password));
186                         }
187
188                         if (!supportsTeamMembershipChanges())
189                             getTeamsFromLdap(ldapConnection, simpleUsername, loggingInUser, user);
190
191                         // Get User Attributes
192                         setUserAttributes(user, loggingInUser);
193
194                         // Push the ldap looked up values to backing file
195                         super.updateUserModel(user);
196                         if (!supportsTeamMembershipChanges()) {
197                             for (TeamModel userTeam : user.teams)
198                                 updateTeamModel(userTeam);
199                         }
200
201                         return user;
62aeb9 202                     }
6cca86 203                 }
301adb 204             } finally {
JM 205                 ldapConnection.close();
6cca86 206             }
f3b625 207         }
6cca86 208         return null;        
JM 209     }
210
27c74e 211     /**
JM 212      * Set the admin attribute from team memberships retrieved from LDAP.
213      * If we are not storing teams in LDAP and/or we have not defined any
214      * administrator teams, then do not change the admin flag.
215      * 
216      * @param user
217      */
f3b625 218     private void setAdminAttribute(UserModel user) {
27c74e 219         if (!supportsTeamMembershipChanges()) {
JM 220             List<String> admins = settings.getStrings(Keys.realm.ldap.admins);
221             // if we have defined administrative teams, then set admin flag
222             // otherwise leave admin flag unchanged
223             if (!ArrayUtils.isEmpty(admins)) {
224                 user.canAdmin = false;
225                 for (String admin : admins) {
226                     if (admin.startsWith("@")) { // Team
227                         if (user.getTeam(admin.substring(1)) != null)
228                             user.canAdmin = true;
229                     } else
230                         if (user.getName().equalsIgnoreCase(admin))
231                             user.canAdmin = true;
232                 }
233             }
234         }
f3b625 235     }
7e0ce4 236     
JC 237     private void setUserAttributes(UserModel user, SearchResultEntry userEntry) {
238         // Is this user an admin?
239         setAdminAttribute(user);
240         
241         // Don't want visibility into the real password, make up a dummy
242         user.password = "StoredInLDAP";
243         
0aa8cf 244         // Get full name Attribute
JM 245         String displayName = settings.getString(Keys.realm.ldap.displayName, "");        
246         if (!StringUtils.isEmpty(displayName)) {
247             // Replace embedded ${} with attributes
248             if (displayName.contains("${")) {
249                 for (Attribute userAttribute : userEntry.getAttributes())
250                     displayName = StringUtils.replace(displayName, "${" + userAttribute.getName() + "}", userAttribute.getValue());
7e0ce4 251
0aa8cf 252                 user.displayName = displayName;
JM 253             } else {
a01257 254                 Attribute attribute = userEntry.getAttribute(displayName);
JM 255                 if (attribute != null && attribute.hasValue()) {
256                     user.displayName = attribute.getValue();
257                 }
0aa8cf 258             }
7e0ce4 259         }
JC 260         
0aa8cf 261         // Get email address Attribute
JM 262         String email = settings.getString(Keys.realm.ldap.email, "");
263         if (!StringUtils.isEmpty(email)) {
264             if (email.contains("${")) {
265                 for (Attribute userAttribute : userEntry.getAttributes())
266                     email = StringUtils.replace(email, "${" + userAttribute.getName() + "}", userAttribute.getValue());
267
268                 user.emailAddress = email;
269             } else {
a01257 270                 Attribute attribute = userEntry.getAttribute(email);
JM 271                 if (attribute != null && attribute.hasValue()) {
272                     user.emailAddress = attribute.getValue();
273                 }
0aa8cf 274             }
7e0ce4 275         }
JC 276     }
6cca86 277
f3b625 278     private void getTeamsFromLdap(LDAPConnection ldapConnection, String simpleUsername, SearchResultEntry loggingInUser, UserModel user) {
JC 279         String loggingInUserDN = loggingInUser.getDN();
280         
281         user.teams.clear();        // Clear the users team memberships - we're going to get them from LDAP
668663 282         String groupBase = settings.getString(Keys.realm.ldap.groupBase, "");
JM 283         String groupMemberPattern = settings.getString(Keys.realm.ldap.groupMemberPattern, "(&(objectClass=group)(member=${dn}))");
f3b625 284         
7e0ce4 285         groupMemberPattern = StringUtils.replace(groupMemberPattern, "${dn}", escapeLDAPSearchFilter(loggingInUserDN));
JC 286         groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
f3b625 287         
JC 288         // Fill in attributes into groupMemberPattern
289         for (Attribute userAttribute : loggingInUser.getAttributes())
7e0ce4 290             groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", escapeLDAPSearchFilter(userAttribute.getValue()));
f3b625 291         
JC 292         SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, groupMemberPattern);
293         if (teamMembershipResult != null && teamMembershipResult.getEntryCount() > 0) {
294             for (int i = 0; i < teamMembershipResult.getEntryCount(); i++) {
295                 SearchResultEntry teamEntry = teamMembershipResult.getSearchEntries().get(i);
296                 String teamName = teamEntry.getAttribute("cn").getValue();
297                 
298                 TeamModel teamModel = getTeamModel(teamName);
299                 if (teamModel == null)
300                     teamModel = createTeamFromLdap(teamEntry);
301                     
302                 user.teams.add(teamModel);
303                 teamModel.addUser(user.getName());
6cca86 304             }
f3b625 305         }
JC 306     }
307     
308     private TeamModel createTeamFromLdap(SearchResultEntry teamEntry) {
309         TeamModel answer = new TeamModel(teamEntry.getAttributeValue("cn"));
6e15cb 310         // potentially retrieve other attributes here in the future
f3b625 311         
JC 312         return answer;        
313     }
314
315     private SearchResult doSearch(LDAPConnection ldapConnection, String base, String filter) {
316         try {
317             return ldapConnection.search(base, SearchScope.SUB, filter);
318         } catch (LDAPSearchException e) {
319             logger.error("Problem Searching LDAP", e);
320             
6cca86 321             return null;
JM 322         }
323     }
f3b625 324     
JC 325     private boolean isAuthenticated(LDAPConnection ldapConnection, String userDn, String password) {
326         try {
7e0ce4 327             // Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN
f3b625 328             ldapConnection.bind(userDn, password);
JC 329             return true;
330         } catch (LDAPException e) {
049a68 331             logger.error("Error authenticating user", e);
f3b625 332             return false;
JC 333         }
334     }
335
6cca86 336     
JM 337     /**
338      * Returns a simple username without any domain prefixes.
339      * 
340      * @param username
341      * @return a simple username
342      */
343     protected String getSimpleUsername(String username) {
344         int lastSlash = username.lastIndexOf('\\');
345         if (lastSlash > -1) {
346             username = username.substring(lastSlash + 1);
347         }
7e0ce4 348         
6cca86 349         return username;
JM 350     }
7e0ce4 351     
JC 352     // From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
353     public static final String escapeLDAPSearchFilter(String filter) {
354         StringBuilder sb = new StringBuilder();
355         for (int i = 0; i < filter.length(); i++) {
356             char curChar = filter.charAt(i);
357             switch (curChar) {
358             case '\\':
359                 sb.append("\\5c");
360                 break;
361             case '*':
362                 sb.append("\\2a");
363                 break;
364             case '(':
365                 sb.append("\\28");
366                 break;
367             case ')':
368                 sb.append("\\29");
369                 break;
370             case '\u0000': 
371                 sb.append("\\00"); 
372                 break;
373             default:
374                 sb.append(curChar);
375             }
376         }
377         return sb.toString();
378     }
6cca86 379 }