001 /** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package org.apache.activemq.jaas; 018 019 import java.io.IOException; 020 import java.security.Principal; 021 import java.text.MessageFormat; 022 import java.util.ArrayList; 023 import java.util.HashSet; 024 import java.util.Hashtable; 025 import java.util.Iterator; 026 import java.util.Map; 027 import java.util.Set; 028 029 import javax.naming.AuthenticationException; 030 import javax.naming.CommunicationException; 031 import javax.naming.Context; 032 import javax.naming.Name; 033 import javax.naming.NameParser; 034 import javax.naming.NamingEnumeration; 035 import javax.naming.NamingException; 036 import javax.naming.directory.Attribute; 037 import javax.naming.directory.Attributes; 038 import javax.naming.directory.DirContext; 039 import javax.naming.directory.InitialDirContext; 040 import javax.naming.directory.SearchControls; 041 import javax.naming.directory.SearchResult; 042 import javax.security.auth.Subject; 043 import javax.security.auth.callback.Callback; 044 import javax.security.auth.callback.CallbackHandler; 045 import javax.security.auth.callback.NameCallback; 046 import javax.security.auth.callback.PasswordCallback; 047 import javax.security.auth.callback.UnsupportedCallbackException; 048 import javax.security.auth.login.FailedLoginException; 049 import javax.security.auth.login.LoginException; 050 import javax.security.auth.spi.LoginModule; 051 052 import org.apache.commons.logging.Log; 053 import org.apache.commons.logging.LogFactory; 054 055 /** 056 * @version $Rev: $ $Date: $ 057 */ 058 public class LDAPLoginModule implements LoginModule { 059 060 private static final String INITIAL_CONTEXT_FACTORY = "initialContextFactory"; 061 private static final String CONNECTION_URL = "connectionURL"; 062 private static final String CONNECTION_USERNAME = "connectionUsername"; 063 private static final String CONNECTION_PASSWORD = "connectionPassword"; 064 private static final String CONNECTION_PROTOCOL = "connectionProtocol"; 065 private static final String AUTHENTICATION = "authentication"; 066 private static final String USER_BASE = "userBase"; 067 private static final String USER_SEARCH_MATCHING = "userSearchMatching"; 068 private static final String USER_SEARCH_SUBTREE = "userSearchSubtree"; 069 private static final String ROLE_BASE = "roleBase"; 070 private static final String ROLE_NAME = "roleName"; 071 private static final String ROLE_SEARCH_MATCHING = "roleSearchMatching"; 072 private static final String ROLE_SEARCH_SUBTREE = "roleSearchSubtree"; 073 private static final String USER_ROLE_NAME = "userRoleName"; 074 075 private static Log log = LogFactory.getLog(LDAPLoginModule.class); 076 077 protected DirContext context; 078 079 private Subject subject; 080 private CallbackHandler handler; 081 private LDAPLoginProperty [] config; 082 private String username; 083 private Set<GroupPrincipal> groups = new HashSet<GroupPrincipal>(); 084 085 public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { 086 this.subject = subject; 087 this.handler = callbackHandler; 088 089 config = new LDAPLoginProperty [] { 090 new LDAPLoginProperty (INITIAL_CONTEXT_FACTORY, (String)options.get(INITIAL_CONTEXT_FACTORY)), 091 new LDAPLoginProperty (CONNECTION_URL, (String)options.get(CONNECTION_URL)), 092 new LDAPLoginProperty (CONNECTION_USERNAME, (String)options.get(CONNECTION_USERNAME)), 093 new LDAPLoginProperty (CONNECTION_PASSWORD, (String)options.get(CONNECTION_PASSWORD)), 094 new LDAPLoginProperty (CONNECTION_PROTOCOL, (String)options.get(CONNECTION_PROTOCOL)), 095 new LDAPLoginProperty (AUTHENTICATION, (String)options.get(AUTHENTICATION)), 096 new LDAPLoginProperty (USER_BASE, (String)options.get(USER_BASE)), 097 new LDAPLoginProperty (USER_SEARCH_MATCHING, (String)options.get(USER_SEARCH_MATCHING)), 098 new LDAPLoginProperty (USER_SEARCH_SUBTREE, (String)options.get(USER_SEARCH_SUBTREE)), 099 new LDAPLoginProperty (ROLE_BASE, (String)options.get(ROLE_BASE)), 100 new LDAPLoginProperty (ROLE_NAME, (String)options.get(ROLE_NAME)), 101 new LDAPLoginProperty (ROLE_SEARCH_MATCHING, (String)options.get(ROLE_SEARCH_MATCHING)), 102 new LDAPLoginProperty (ROLE_SEARCH_SUBTREE, (String)options.get(ROLE_SEARCH_SUBTREE)), 103 new LDAPLoginProperty (USER_ROLE_NAME, (String)options.get(USER_ROLE_NAME)), 104 }; 105 } 106 107 public boolean login() throws LoginException { 108 109 Callback[] callbacks = new Callback[2]; 110 111 callbacks[0] = new NameCallback("User name"); 112 callbacks[1] = new PasswordCallback("Password", false); 113 try { 114 handler.handle(callbacks); 115 } catch (IOException ioe) { 116 throw (LoginException)new LoginException().initCause(ioe); 117 } catch (UnsupportedCallbackException uce) { 118 throw (LoginException)new LoginException().initCause(uce); 119 } 120 121 String password; 122 123 username = ((NameCallback)callbacks[0]).getName(); 124 if (username == null) 125 return false; 126 127 if (((PasswordCallback)callbacks[1]).getPassword() != null) 128 password = new String(((PasswordCallback)callbacks[1]).getPassword()); 129 else 130 password=""; 131 132 try { 133 boolean result = authenticate(username, password); 134 if (!result) { 135 throw new FailedLoginException(); 136 } else { 137 return true; 138 } 139 } catch (Exception e) { 140 throw (LoginException)new LoginException("LDAP Error").initCause(e); 141 } 142 } 143 144 public boolean logout() throws LoginException { 145 username = null; 146 return true; 147 } 148 149 public boolean commit() throws LoginException { 150 Set<Principal> principals = subject.getPrincipals(); 151 principals.add(new UserPrincipal(username)); 152 Iterator<GroupPrincipal> iter = groups.iterator(); 153 while (iter.hasNext()) { 154 principals.add(iter.next()); 155 } 156 return true; 157 } 158 159 public boolean abort() throws LoginException { 160 username = null; 161 return true; 162 } 163 164 protected void close(DirContext context) { 165 try { 166 context.close(); 167 } catch (Exception e) { 168 log.error(e); 169 } 170 } 171 172 protected boolean authenticate(String username, String password) throws Exception { 173 174 MessageFormat userSearchMatchingFormat; 175 boolean userSearchSubtreeBool; 176 177 DirContext context = null; 178 context = open(); 179 180 if (!isLoginPropertySet(USER_SEARCH_MATCHING)) 181 return false; 182 183 userSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(USER_SEARCH_MATCHING)); 184 userSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(USER_SEARCH_SUBTREE)).booleanValue(); 185 186 try { 187 188 String filter = userSearchMatchingFormat.format(new String[] { 189 username 190 }); 191 SearchControls constraints = new SearchControls(); 192 if (userSearchSubtreeBool) { 193 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); 194 } else { 195 constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE); 196 } 197 198 // setup attributes 199 ArrayList<String> list = new ArrayList<String>(); 200 if (isLoginPropertySet(USER_ROLE_NAME)) { 201 list.add(getLDAPPropertyValue(USER_ROLE_NAME)); 202 } 203 String[] attribs = new String[list.size()]; 204 list.toArray(attribs); 205 constraints.setReturningAttributes(attribs); 206 207 NamingEnumeration results = context.search(getLDAPPropertyValue(USER_BASE), filter, constraints); 208 209 if (results == null || !results.hasMore()) { 210 return false; 211 } 212 213 SearchResult result = (SearchResult)results.next(); 214 215 if (results.hasMore()) { 216 // ignore for now 217 } 218 NameParser parser = context.getNameParser(""); 219 Name contextName = parser.parse(context.getNameInNamespace()); 220 Name baseName = parser.parse(getLDAPPropertyValue(USER_BASE)); 221 Name entryName = parser.parse(result.getName()); 222 Name name = contextName.addAll(baseName); 223 name = name.addAll(entryName); 224 String dn = name.toString(); 225 226 Attributes attrs = result.getAttributes(); 227 if (attrs == null) { 228 return false; 229 } 230 ArrayList<String> roles = null; 231 if (isLoginPropertySet(USER_ROLE_NAME)) { 232 roles = addAttributeValues(getLDAPPropertyValue(USER_ROLE_NAME), attrs, roles); 233 } 234 235 // check the credentials by binding to server 236 if (bindUser(context, dn, password)) { 237 // if authenticated add more roles 238 roles = getRoles(context, dn, username, roles); 239 for (int i = 0; i < roles.size(); i++) { 240 groups.add(new GroupPrincipal(roles.get(i))); 241 } 242 } else { 243 return false; 244 } 245 } catch (CommunicationException e) { 246 247 } catch (NamingException e) { 248 if (context != null) { 249 close(context); 250 } 251 return false; 252 } 253 254 return true; 255 } 256 257 protected ArrayList<String> getRoles(DirContext context, String dn, String username, ArrayList<String> currentRoles) throws NamingException { 258 ArrayList<String> list = currentRoles; 259 MessageFormat roleSearchMatchingFormat; 260 boolean roleSearchSubtreeBool; 261 roleSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(ROLE_SEARCH_MATCHING)); 262 roleSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(ROLE_SEARCH_SUBTREE)).booleanValue(); 263 264 if (list == null) { 265 list = new ArrayList<String>(); 266 } 267 if (!isLoginPropertySet(ROLE_NAME)) { 268 return list; 269 } 270 String filter = roleSearchMatchingFormat.format(new String[] { 271 doRFC2254Encoding(dn), username 272 }); 273 274 SearchControls constraints = new SearchControls(); 275 if (roleSearchSubtreeBool) { 276 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); 277 } else { 278 constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE); 279 } 280 NamingEnumeration results = context.search(getLDAPPropertyValue(ROLE_BASE), filter, constraints); 281 while (results.hasMore()) { 282 SearchResult result = (SearchResult)results.next(); 283 Attributes attrs = result.getAttributes(); 284 if (attrs == null) { 285 continue; 286 } 287 list = addAttributeValues(getLDAPPropertyValue(ROLE_NAME), attrs, list); 288 } 289 return list; 290 291 } 292 293 protected String doRFC2254Encoding(String inputString) { 294 StringBuffer buf = new StringBuffer(inputString.length()); 295 for (int i = 0; i < inputString.length(); i++) { 296 char c = inputString.charAt(i); 297 switch (c) { 298 case '\\': 299 buf.append("\\5c"); 300 break; 301 case '*': 302 buf.append("\\2a"); 303 break; 304 case '(': 305 buf.append("\\28"); 306 break; 307 case ')': 308 buf.append("\\29"); 309 break; 310 case '\0': 311 buf.append("\\00"); 312 break; 313 default: 314 buf.append(c); 315 break; 316 } 317 } 318 return buf.toString(); 319 } 320 321 protected boolean bindUser(DirContext context, String dn, String password) throws NamingException { 322 boolean isValid = false; 323 324 context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn); 325 context.addToEnvironment(Context.SECURITY_CREDENTIALS, password); 326 try { 327 context.getAttributes("", null); 328 isValid = true; 329 } catch (AuthenticationException e) { 330 isValid = false; 331 log.debug("Authentication failed for dn=" + dn); 332 } 333 334 if (isLoginPropertySet(CONNECTION_USERNAME)) { 335 context.addToEnvironment(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME)); 336 } else { 337 context.removeFromEnvironment(Context.SECURITY_PRINCIPAL); 338 } 339 340 if (isLoginPropertySet(CONNECTION_PASSWORD)) { 341 context.addToEnvironment(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD)); 342 } else { 343 context.removeFromEnvironment(Context.SECURITY_CREDENTIALS); 344 } 345 346 return isValid; 347 } 348 349 private ArrayList<String> addAttributeValues(String attrId, Attributes attrs, ArrayList<String> values) throws NamingException { 350 351 if (attrId == null || attrs == null) { 352 return values; 353 } 354 if (values == null) { 355 values = new ArrayList<String>(); 356 } 357 Attribute attr = attrs.get(attrId); 358 if (attr == null) { 359 return values; 360 } 361 NamingEnumeration e = attr.getAll(); 362 while (e.hasMore()) { 363 String value = (String)e.next(); 364 values.add(value); 365 } 366 return values; 367 } 368 369 protected DirContext open() throws NamingException { 370 try { 371 Hashtable<String, String> env = new Hashtable<String, String>(); 372 env.put(Context.INITIAL_CONTEXT_FACTORY, getLDAPPropertyValue(INITIAL_CONTEXT_FACTORY)); 373 if (isLoginPropertySet(CONNECTION_USERNAME)) { 374 env.put(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME)); 375 } 376 if (isLoginPropertySet(CONNECTION_PASSWORD)) { 377 env.put(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD)); 378 } 379 env.put(Context.SECURITY_PROTOCOL, getLDAPPropertyValue(CONNECTION_PROTOCOL)); 380 env.put(Context.PROVIDER_URL, getLDAPPropertyValue(CONNECTION_URL)); 381 env.put(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(AUTHENTICATION)); 382 context = new InitialDirContext(env); 383 384 } catch (NamingException e) { 385 log.error(e); 386 throw e; 387 } 388 return context; 389 } 390 391 private String getLDAPPropertyValue (String propertyName){ 392 for (int i=0; i < config.length; i++ ) 393 if (config[i].getPropertyName() == propertyName) 394 return config[i].getPropertyValue(); 395 return null; 396 } 397 398 private boolean isLoginPropertySet(String propertyName) { 399 for (int i=0; i < config.length; i++ ) { 400 if (config[i].getPropertyName() == propertyName && config[i].getPropertyValue() != null) 401 return true; 402 } 403 return false; 404 } 405 406 }