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