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