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