001/** 002 * Copyright (C) 2009 "Darwin V. Felix" <darwinfelix@users.sourceforge.net> 003 * 004 * This library is free software; you can redistribute it and/or 005 * modify it under the terms of the GNU Lesser General Public 006 * License as published by the Free Software Foundation; either 007 * version 2.1 of the License, or (at your option) any later version. 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * You should have received a copy of the GNU Lesser General Public 015 * License along with this library; if not, write to the Free Software 016 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 017 */ 018 019package net.sourceforge.spnego; 020 021import java.io.FileInputStream; 022import java.io.IOException; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.HashMap; 026import java.util.HashSet; 027import java.util.Hashtable; 028import java.util.List; 029import java.util.Map; 030import java.util.Properties; 031import java.util.Set; 032import java.util.concurrent.locks.Lock; 033import java.util.concurrent.locks.ReentrantReadWriteLock; 034import java.util.logging.Logger; 035 036import javax.naming.Context; 037import javax.naming.NamingEnumeration; 038import javax.naming.NamingException; 039import javax.naming.directory.Attribute; 040import javax.naming.directory.Attributes; 041import javax.naming.directory.SearchControls; 042import javax.naming.directory.SearchResult; 043import javax.naming.ldap.InitialLdapContext; 044import javax.naming.ldap.LdapContext; 045 046/** 047 * The <code>LdapAccessControl</code> class is a reference implementation 048 * of the {@link UserAccessControl} interface. This class only performs 049 * user authorization (authZ) and not user authentication (authN). This 050 * class implements an authZ model in a similar style as the 051 * <a href="https://en.wikipedia.org/wiki/Role-based_access_control">Role 052 * Based Access Control</a> (RBAC) model and the 053 * <a href="https://en.wikipedia.org/wiki/Attribute-Based_Access_Control">Attribute 054 * Based Access Control</a> (ABAC) model. However, this pedagogical implementation 055 * is much simpler and limited than the two formal models. 056 * 057 * <p> 058 * Usage examples and semantics can be found in the javadoc of the interface 059 * that this class implements ({@link UserAccessControl}). 060 * </p> 061 * 062 * <p> 063 * The <code>LdapAccessControl</code> implementation makes calls to an 064 * LDAP server (e.g. Microsoft's Active Directory (AD) server) when performing 065 * a lookup to determine if a user has one or more attributes defined. The 066 * LDAP queries are based on LDAP search/fiter criteria(s) defined at the time 067 * an instance of this class is placed into service. An instance of this class 068 * is considered to be in service after invoking the <code>init</code> method. 069 * </p> 070 * 071 * <p> 072 * In the SPNEGO Library, the <code>SpnegoHttpFilter</code> class is the mechanism 073 * that performs user authentication (authN) whilst the <code>LdapAccessControl</code> 074 * class is the default mechanism that performs user authorization (authZ). This 075 * default can be replaced by any class that implement the <code>UserAccessControl</code> 076 * interface. To change the default, specify the new class in the SPNEGO Library's 077 * filter definition section of the web.xml file. 078 * </p> 079 * 080 * <p> 081 * Authorization (authZ) is an optional feature of the SPNEGO Library. The SPNEGO 082 * Library provides an interface, {@link SpnegoAccessControl}, to applications 083 * that need authZ capability. Applications can check a user's authZ by calling 084 * methods defined in the <code>SpnegoAccessControl</code> interface. 085 * </p> 086 * 087 * <p> 088 * The <code>LdapAccessControl</code> class is configured within the same web.xml 089 * filter section as the <code>SpnegoHttpFilter</code> class. The configuration is 090 * specified by adding additional filter parameters to the <code>SpnegoHttpFilter</code> 091 * filter definition. 092 * </p> 093 * 094 * <p> 095 * <b>Example web.xml Configuration:</b> 096 * <pre> 097 * <filter> 098 * <filter-name>SpnegoHttpFilter</filter-name> 099 * <filter-class>net.sourceforge.spnego.SpnegoHttpFilter</filter-class> 100 * 101 * <!-- spnego http filter params (authN) --> 102 * ... existing authN params here just as before ... 103 * 104 * <!-- spnego http filter params (authZ) --> 105 * <init-param> 106 * <param-name>spnego.authz.class</param-name> 107 * <param-value>net.sourceforge.spnego.LdapAccessControl</param-value> 108 * </init-param> 109 * <init-param> 110 * <param-name>spnego.authz.ldap.url</param-name> 111 * <param-value>ldap://athena.local:389</param-value> 112 * </init-param> 113 * 114 * <!-- an example user-defined resource label --> 115 * <init-param> 116 * <param-name>spnego.authz.resource.name.1</param-name> 117 * <param-value>admin-buttons</param-value> 118 * </init-param> 119 * <init-param> 120 * <param-name>spnego.authz.resource.access.1</param-name> 121 * <param-value>Biz. Analyst,Los Angeles,IT Group</param-value> 122 * </init-param> 123 * <init-param> 124 * <param-name>spnego.authz.resource.type.1</param-name> 125 * <param-value>has</param-value> 126 * </init-param> 127 * 128 * <!-- CDATA required since specifying filter(s) in web.xml (vs. a policy file) --> 129 * <!-- also notice the %1$s and the %2$s tokens (always required) --> 130 * <init-param> 131 * <param-name>spnego.authz.ldap.filter.1</param-name> 132 * <param-value><![CDATA[(&(sAMAccountName=%1$s)(memberOf:1.2.840.113556.1.4.1941:=CN=%2$s,OU=Groups,OU=Los Angeles,DC=athena,DC=local))]]></param-value> 133 * </init-param> 134 * <init-param> 135 * <param-name>spnego.authz.ldap.filter.2</param-name> 136 * <param-value><![CDATA[(&(sAMAccountType=805306368)(sAMAccountName=%1$s)(&(sAMAccountType=805306368)(department=%2$s)))]]></param-value> 137 * </init-param> 138 * </filter> 139 * </pre> 140 * </p> 141 * 142 * <p> 143 * As an alternative option, the <code>spnego.authz.ldap.filter.[i]</code> parameters and 144 * the <code>spnego.authz.resource.[name|access|type].[i]</code> parameters may be specified 145 * in a policy file. 146 * </p> 147 * 148 * <p> 149 * <b>Example Policy File Configuration:</b> 150 * <pre> 151 * <filter> 152 * <filter-name>SpnegoHttpFilter</filter-name> 153 * <filter-class>net.sourceforge.spnego.SpnegoHttpFilter</filter-class> 154 * 155 * <!-- spnego http filter params (authN) --> 156 * ... existing authN params here just as before ... 157 * 158 * <!-- spnego http filter params (authZ) --> 159 * <init-param> 160 * <param-name>spnego.authz.class</param-name> 161 * <param-value>net.sourceforge.spnego.LdapAccessControl</param-value> 162 * </init-param> 163 * <init-param> 164 * <param-name>spnego.authz.ldap.url</param-name> 165 * <param-value>ldap://athena.local:389</param-value> 166 * </init-param> 167 * <init-param> 168 * <param-name>spnego.authz.policy.file</param-name> 169 * <param-value>C:/Apache Software Foundation/Tomcat 7.0/conf/spnego.policy</param-value> 170 * </init-param> 171 * </filter> 172 * </pre> 173 * 174 * Policy file contents: 175 * <pre> 176 * # an example user-defined resource label 177 * spnego.authz.resource.name.1=admin-buttons 178 * spnego.authz.resource.access.1=Biz. Analyst,Los Angeles,IT Group 179 * spnego.authz.resource.type.1=has 180 * 181 * # do NOT use CDATA like in the web.xml file 182 * # the %1$s and the %2$s tokens are always required 183 * spnego.authz.ldap.filter.1=(&(sAMAccountName=%1$s)(memberOf:1.2.840.113556.1.4.1941:=CN=%2$s,OU=Groups,OU=Los Angeles,DC=athena,DC=local)) 184 * spnego.authz.ldap.filter.2=(&(sAMAccountType=805306368)(sAMAccountName=%1$s)(&(sAMAccountType=805306368)(department=%2$s))) 185 * </pre> 186 * </p> 187 * 188 * <p> 189 * For more information on how a web application/service can leverage access controls 190 * or to view some usage examples, please read the javadoc of the {@link SpnegoAccessControl} 191 * interface and the javadoc of the {@link UserAccessControl} interface. 192 * </p> 193 * 194 * <p> 195 * Also, take a look at the <a href="http://spnego.sourceforge.net/reference_docs.html" 196 * target="_blank">reference docs</a> for a complete list of configuration parameters. 197 * </p> 198 * 199 * <p> 200 * Finally, to see a working example and instructions, take a look at the 201 * <a href="http://spnego.sourceforge.net/user_access_control.html" 202 * target="_blank">authZ for standalone apps</a> example and the 203 * <a href="http://spnego.sourceforge.net/enable_authZ_ldap.html" 204 * target="_blank">enable authZ with LDAP</a> guide. 205 * </p> 206 * 207 * 208 * @author Darwin V. Felix 209 * 210 */ 211public class LdapAccessControl implements UserAccessControl { 212 213 private static final Logger LOGGER = 214 Logger.getLogger(LdapAccessControl.class.getName()); 215 216 private static final String POLICY_FILE = "spnego.authz.policy.file"; 217 218 private static final String SERVER_REALM = "spnego.server.realm"; 219 220 private static final String LDAP_FACTORY = "spnego.authz.ldap.factory"; 221 222 private static final String LDAP_AUTHN = "spnego.authz.ldap.authn"; 223 224 private static final String LDAP_POOL = "spnego.authz.ldap.pool"; 225 226 private static final String LDAP_DEECE = "spnego.authz.ldap.deecee"; 227 228 private static final String LDAP_URL = "spnego.authz.ldap.url"; 229 230 private static final String LDAP_USERNAME = "spnego.authz.ldap.username"; 231 232 private static final String KRB5_USERNAME = "spnego.preauth.username"; 233 234 private static final String LDAP_PASSWORD = "spnego.authz.ldap.password"; 235 236 private static final String KRB5_PASSWORD = "spnego.preauth.password"; 237 238 private static final String TTL = "spnego.authz.ttl"; 239 240 private static final String UNIQUE = "spnego.authz.unique"; 241 242 private static final String PREFIX_FILTER = "spnego.authz.ldap.filter."; 243 244 private static final String PREFIX_NAME = "spnego.authz.resource.name."; 245 246 private static final String PREFIX_TYPE = "spnego.authz.resource.type."; 247 248 private static final String PREFIX_ACCESS = "spnego.authz.resource.access."; 249 250 private static final String HAS = "has"; 251 252 private static final String ANY = "any"; 253 254 /** case-sensitive. e.g. values mail,department,name,memberOf, etc. */ 255 private static final String USER_INFO = "spnego.authz.user.info"; 256 257 /** e.g. (&(sAMAccountType=805306368)(sAMAccountName=%1$s)) */ 258 private static final String USER_INFO_FILTER = "spnego.authz.ldap.user.filter"; 259 260 /** default is 20 minutes. */ 261 private static final long DEFAULT_TTL = 20 * 60 * 1000; 262 263 /** maximum number of ldap filters is 200 */ 264 private static final int MAX_NUM_FILTERS = 200; 265 266 /** read lock for reading instance variables and write lock for ldap search. */ 267 private final transient ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); 268 private final transient Lock readLock = readWriteLock.readLock(); 269 private final transient Lock writeLock = readWriteLock.writeLock(); 270 271 /** cache LDAP results to minimize trips to ldap server. */ 272 private final transient Map<String, Long> matchedList = new HashMap<String, Long>(); 273 private final transient Map<String, Long> unMatchedList = new HashMap<String, Long>(); 274 private final transient Map<String, UserInfo> userInfoList = new HashMap<String, UserInfo>(); 275 276 private transient Hashtable<String, String> environment; 277 private transient SearchControls srchCntrls; 278 279 /** DC= base portionS of the ldap search filter. */ 280 private transient String deecee = ""; 281 282 /** ldap search filter(s). */ 283 private transient Set<String> policy = new HashSet<String>(); 284 285 /** determines how long to keep in cache. */ 286 private transient long expiration = DEFAULT_TTL; 287 288 /** determines if an exception should be thrown if it finds a duplicate. */ 289 private transient boolean uniqueOnly = true; 290 291 /** access resources by using a user-defined label. */ 292 private transient Map<String, Map<String, String[]>> resources = 293 new HashMap<String, Map<String, String[]>>(); 294 295 private transient List<String> userInfoLabels = new ArrayList<String>(); 296 297 private transient String userInfoFilter; 298 299 /** 300 * Default constructor. 301 */ 302 public LdapAccessControl() { 303 // default constructor 304 } 305 306 @Override 307 public void destroy() { 308 LOGGER.info("destroy()..."); 309 this.writeLock.lock(); 310 try { 311 this.matchedList.clear(); 312 this.unMatchedList.clear(); 313 this.environment.clear(); 314 this.environment = null; 315 this.srchCntrls = null; 316 this.deecee = ""; 317 this.policy.clear(); 318 this.expiration = DEFAULT_TTL; 319 this.resources.clear(); 320 this.userInfoLabels.clear(); 321 this.userInfoFilter = null; 322 } finally { 323 this.writeLock.unlock(); 324 } 325 } 326 327 /* 328 * (non-Javadoc) 329 * @see net.sourceforge.spnego.UserAccessControl#anyRole(java.lang.String, java.lang.String[]) 330 */ 331 @Override 332 public boolean anyRole(final String username, final String... attributes) { 333 for (String role : attributes) { 334 if (hasRole(username, role)) { 335 return true; 336 } 337 } 338 return false; 339 } 340 341 /* 342 * (non-Javadoc) 343 * @see net.sourceforge.spnego.UserAccessControl#hasRole(java.lang.String, java.lang.String) 344 */ 345 @Override 346 public boolean hasRole(final String username, final String attribute) { 347 final String key = username + "_attr_" + attribute; 348 final long now = System.currentTimeMillis(); 349 350 try { 351 if (!matchedExpired(key, now)) { 352 return true; 353 } 354 355 if (!unMatchedExpired(key, now)) { 356 return false; 357 } 358 359 // query AD to update both MapS and expiration time 360 LOGGER.fine("username: " + username + "; role: " + attribute); 361 362 this.writeLock.lock(); 363 try { 364 // remove from cache if exists 365 this.matchedList.remove(key); 366 this.unMatchedList.remove(key); 367 368 int count = 0; 369 final LdapContext context = new InitialLdapContext(environment, null); 370 for (String filter : this.policy) { 371 // perform AD lookup add to cache 372 final NamingEnumeration<SearchResult> results = 373 context.search(this.deecee 374 , String.format(filter, username, attribute) 375 , this.srchCntrls); 376 377 final boolean found = results.hasMoreElements(); 378 results.close(); 379 380 // add to cache 381 if (found) { 382 count++; 383 //LOGGER.info("add attribute to matchedList: " + attribute); 384 this.matchedList.put(key, System.currentTimeMillis()); 385 if (!this.uniqueOnly) { 386 break; 387 } 388 } 389 390 // check if we have a duplicate attribute 391 if (count > 1 && this.uniqueOnly) { 392 this.matchedList.remove(key); 393 throw new IllegalArgumentException("Uniqueness property violated. " 394 + "Found duplicate role/attribute:" + attribute 395 + ". This MAY be caused by an improper policy definition" 396 + "; filter=" + filter 397 + "; policy=" + this.policy); 398 } 399 } 400 context.close(); 401 402 if (0 == count) { 403 //LOGGER.info("add attribute to unMatchedList: " + attribute); 404 this.unMatchedList.put(key, System.currentTimeMillis()); 405 } else { 406 cacheUserInfo(username); 407 } 408 409 } finally { 410 this.writeLock.unlock(); 411 } 412 } catch (NamingException lex) { 413 LOGGER.severe(lex.getMessage()); 414 throw new RuntimeException(lex); 415 } 416 417 return hasRole(username, attribute); 418 } 419 420 /* 421 * (non-Javadoc) 422 * @see net.sourceforge.spnego.UserAccessControl#hasRole(java.lang.String, java.lang.String, java.lang.String[]) 423 */ 424 @Override 425 public boolean hasRole(final String username, final String attributeX, final String... attributeYs) { 426 427 // assert 428 if (null == attributeYs || 0 == attributeYs.length) { 429 final String errorMsg = "Must provide at least two parameters"; 430 LOGGER.severe(errorMsg); 431 throw new IllegalArgumentException(errorMsg); 432 } 433 434 boolean found = false; 435 final boolean featX = hasRole(username, attributeX); 436 437 for (String featY : attributeYs) { 438 found = featX && hasRole(username, featY); 439 if (found) { 440 break; 441 } 442 } 443 444 return found; 445 } 446 447 /* 448 * (non-Javadoc) 449 * @see net.sourceforge.spnego.UserAccessControl#anyAccess(java.lang.String, java.lang.String[]) 450 */ 451 @Override 452 public boolean anyAccess(final String username, final String... resources) { 453 for (String resource : resources) { 454 if (hasAccess(username, resource)) { 455 return true; 456 } 457 } 458 return false; 459 } 460 461 /* 462 * (non-Javadoc) 463 * @see net.sourceforge.spnego.UserAccessControl#hasAccess(java.lang.String, java.lang.String) 464 */ 465 @Override 466 public boolean hasAccess(final String username, final String resource) { 467 final String key = username + "_res_" + resource; 468 final long now = System.currentTimeMillis(); 469 470 if (!matchedExpired(key, now)) { 471 return true; 472 } 473 474 if (!unMatchedExpired(key, now)) { 475 return false; 476 } 477 478 // query AD to update both MapS and expiration time 479 LOGGER.fine("username: " + username + "; resource: " + resource); 480 481 boolean matched = false; 482 boolean containsHas = false; 483 boolean containsAny = false; 484 String[] attributes = new String[] {}; 485 486 this.readLock.lock(); 487 try { 488 // assert 489 if (!this.resources.containsKey(resource)) { 490 throw new IllegalArgumentException( 491 "Policy not found for user-defined Resource labeled: " + resource); 492 } 493 494 containsHas = this.resources.get(resource).containsKey(HAS); 495 containsAny = this.resources.get(resource).containsKey(ANY); 496 if (containsHas) { 497 attributes = this.resources.get(resource).get(HAS); 498 } else if (containsAny) { 499 attributes = this.resources.get(resource).get(ANY); 500 } 501 } finally { 502 this.readLock.unlock(); 503 } 504 505 if (containsHas) { 506 if (attributes.length > 1) { 507 matched = this.hasRole(username, attributes[0] 508 , Arrays.copyOfRange(attributes, 1, attributes.length)); 509 } else if (attributes.length == 1) { 510 matched = this.hasRole(username, attributes[0]); 511 } else { 512 throw new IllegalStateException("No attribute(s) defined for resource: " + resource); 513 } 514 } else if (containsAny) { 515 matched = this.anyRole(username, attributes); 516 } else { 517 throw new UnsupportedOperationException("Allowed resource.type(s): [any|has]"); 518 } 519 520 this.writeLock.lock(); 521 try { 522 if (matched) { 523 //LOGGER.info("add resource to matchedList: " + resource); 524 this.matchedList.put(key, now); 525 } else { 526 //LOGGER.info("add resource to unMatchedList: " + resource); 527 this.unMatchedList.put(key, now); 528 } 529 } finally { 530 this.writeLock.unlock(); 531 } 532 533 return matched; 534 } 535 536 /* 537 * (non-Javadoc) 538 * @see net.sourceforge.spnego.UserAccessControl#hasAccess(java.lang.String, java.lang.String, java.lang.String[]) 539 */ 540 @Override 541 public boolean hasAccess(final String username, final String resourceX, final String... resourceYs) { 542 543 // assert 544 if (null == resourceYs || 0 == resourceYs.length) { 545 final String errorMsg = "Must provide at least two parameters"; 546 LOGGER.severe(errorMsg); 547 throw new IllegalArgumentException(errorMsg); 548 } 549 550 boolean found = false; 551 final boolean resX = hasAccess(username, resourceX); 552 553 for (String resY : resourceYs) { 554 found = resX && hasAccess(username, resY); 555 if (found) { 556 break; 557 } 558 } 559 560 return found; 561 } 562 563 /** 564 * Returns a user info object if specified in web.xml or the spnego.policy file. 565 * 566 * <p>Case-sensitive</br> 567 * 568 * <p> 569 * <b>web.xml Example:</b></br /> 570 * <pre> 571 * ... 572 * <init-param> 573 * <param-name>spnego.authz.user.info</param-name> 574 * <param-value>mail,department,memberOf,displayName</param-value> 575 * </init-param> 576 * <init-param> 577 * <param-name>spnego.authz.ldap.user.filter</param-name> 578 * <param-value><![CDATA[(&(sAMAccountType=805306368)(sAMAccountName=%1$s))]]></param-value> 579 * </init-param> 580 * ... 581 * </pre> 582 * </p> 583 * 584 * <p> 585 * <b>spnego.policy File Example:</b><br /> 586 * <pre> 587 * ... 588 * # case-sensitive 589 * spnego.authz.user.info=mail,department,memberOf,displayName 590 * spnego.authz.ldap.user.filter=(&(sAMAccountType=805306368)(sAMAccountName=%1$s)) 591 * ... 592 * </pre> 593 * </p> 594 * </p> 595 * 596 * @param username e.g. dfelix 597 * @return UserInfo object with the specified ldap attributes 598 */ 599 @Override 600 public UserInfo getUserInfo(final String username) { 601 final long now = System.currentTimeMillis(); 602 final boolean expired = matchedExpired(username, now); 603 604 this.readLock.lock(); 605 try { 606 if (!expired) { 607 return this.userInfoList.get(username); 608 } 609 } finally { 610 this.readLock.unlock(); 611 } 612 613 this.writeLock.lock(); 614 try { 615 return cacheUserInfo(username); 616 } catch (NamingException nex) { 617 final String errorMessage = "Could not get user info for: " + username; 618 LOGGER.warning(errorMessage); 619 throw new IllegalStateException(errorMessage, nex); 620 } finally { 621 this.writeLock.unlock(); 622 } 623 } 624 625 @Override 626 public void init(final Properties props) { 627 LOGGER.info("init()..."); 628 629 this.readLock.lock(); 630 try { 631 if (this.environment != null) { 632 // must call the destroy method before re-initializing 633 throw new IllegalStateException("LdapAccessControl already initialized"); 634 } 635 } finally { 636 this.readLock.unlock(); 637 } 638 639 final String policyFile = props.getProperty(POLICY_FILE, ""); 640 final Properties policies = new Properties(); 641 if (!policyFile.isEmpty()) { 642 try { 643 LOGGER.info("policy file: " + policyFile); 644 final FileInputStream fis =new FileInputStream(policyFile); 645 try { 646 policies.load(fis); 647 } finally { 648 fis.close(); 649 } 650 } catch (IOException e) { 651 throw new IllegalArgumentException( 652 "Policy File NOT Found: " + policyFile, e); 653 } 654 } 655 656 // use defaults if not specified 657 final Hashtable<String, String> env = new Hashtable<String, String>(); 658 env.put(Context.INITIAL_CONTEXT_FACTORY 659 , policies.getProperty(LDAP_FACTORY 660 , props.getProperty(LDAP_FACTORY 661 , "com.sun.jndi.ldap.LdapCtxFactory"))); 662 env.put(Context.SECURITY_AUTHENTICATION 663 , policies.getProperty(LDAP_AUTHN 664 , props.getProperty(LDAP_AUTHN 665 , "Simple"))); 666 env.put("com.sun.jndi.ldap.connect.pool" 667 , policies.getProperty(LDAP_POOL 668 , props.getProperty(LDAP_POOL 669 , "true"))); 670 671 // if deecee was not provided, calculate using server's realm 672 String dc = policies.getProperty(LDAP_DEECE 673 , props.getProperty(LDAP_DEECE, "")); 674 if (dc.isEmpty()) { 675 final String tmp = props.getProperty(SERVER_REALM 676 , policies.getProperty(SERVER_REALM, "")); 677 if (tmp.trim().isEmpty()) { 678 throw new IllegalArgumentException("MUST provide the serve's deecee. " 679 + " specify a value for the " + LDAP_DEECE + " property."); 680 } 681 dc = "DC=" + tmp.replaceAll("\\.", ",DC="); 682 } 683 LOGGER.info(dc); 684 685 // assert that an ldap url was provided 686 if (policies.getProperty(LDAP_URL, props.getProperty(LDAP_URL, "")).isEmpty()) { 687 final String errorMessage = "Must provide a value for the spnego.authz.ldap.url parameter"; 688 LOGGER.severe(errorMessage); 689 throw new IllegalStateException(errorMessage); 690 } else { 691 env.put(Context.PROVIDER_URL 692 , policies.getProperty(LDAP_URL, props.getProperty(LDAP_URL))); 693 LOGGER.info("ldap provider url: " + env.get(Context.PROVIDER_URL)); 694 } 695 696 // if username/password not provided, default to krb5 username/password 697 // if nothing is specified because a keytab file was specified... error. 698 if (policies.getProperty(LDAP_USERNAME, props.getProperty(LDAP_USERNAME 699 , props.getProperty(KRB5_USERNAME, policies.getProperty(KRB5_USERNAME, "")))).isEmpty()) { 700 final String errorMessage = "Must provide a username to use for connecting to the LDAP server"; 701 LOGGER.severe(errorMessage); 702 throw new IllegalArgumentException(errorMessage); 703 } 704 if (policies.getProperty(LDAP_PASSWORD, props.getProperty(LDAP_PASSWORD 705 , props.getProperty(KRB5_PASSWORD, policies.getProperty(KRB5_PASSWORD, "")))).isEmpty()) { 706 final String errorMessage = "Must provide a password to use for connecting to the LDAP server"; 707 LOGGER.severe(errorMessage); 708 throw new IllegalArgumentException(errorMessage); 709 } 710 711 env.put(Context.SECURITY_PRINCIPAL 712 , policies.getProperty(LDAP_USERNAME, props.getProperty(LDAP_USERNAME 713 , props.getProperty(KRB5_USERNAME, policies.getProperty(KRB5_USERNAME))))); 714 env.put(Context.SECURITY_CREDENTIALS 715 , policies.getProperty(LDAP_PASSWORD, props.getProperty(LDAP_PASSWORD 716 , props.getProperty(KRB5_PASSWORD, policies.getProperty(KRB5_PASSWORD))))); 717 LOGGER.info("ldap security principal: " + env.get(Context.SECURITY_PRINCIPAL)); 718 719 // specifiy how many minutes the cache is good for 720 // used to control when to re-query/perform another ldap search 721 long ttl = DEFAULT_TTL; 722 ttl = Long.parseLong(policies.getProperty(TTL, props.getProperty(TTL, "-1"))); 723 LOGGER.info("spnego.authz.ttl: " + ttl); 724 725 // determine if we're allowed to violate the uniqueness property 726 this.uniqueOnly = Boolean.parseBoolean(policies.getProperty(UNIQUE 727 , props.getProperty(UNIQUE, "true"))); 728 729 LOGGER.info("uniqueness property enabled: " + uniqueOnly); 730 731 this.writeLock.lock(); 732 try { 733 this.deecee = dc; 734 735 // create policy statements 736 loadPolicies(policies, props); 737 738 // optional labels for resources 739 loadResourceNames(policies, props); 740 741 if (ttl < 1) { 742 this.expiration = DEFAULT_TTL; 743 } else { 744 this.expiration = ttl * 60 * 1000; 745 } 746 LOGGER.info("cache expiration in millis: " + this.expiration); 747 748 this.srchCntrls = new SearchControls(); 749 this.srchCntrls.setSearchScope(SearchControls.SUBTREE_SCOPE); 750 this.environment = env; 751 752 final String[] labels = policies.getProperty(USER_INFO 753 , props.getProperty(USER_INFO, "")).split(","); 754 755 LOGGER.info("UserInfo label count: " + labels.length); 756 for (String label : labels) { 757 LOGGER.info(label); 758 this.userInfoLabels.add(label.trim()); 759 } 760 761 this.userInfoFilter = policies.getProperty(USER_INFO_FILTER 762 , props.getProperty(USER_INFO_FILTER, "")); 763 764 LOGGER.info("UserInfo filter: " + this.userInfoFilter); 765 766 } finally { 767 this.writeLock.unlock(); 768 } 769 } 770 771 private boolean matchedExpired(final String key, final long now) { 772 final boolean matched = this.matchedList.containsKey(key); 773 boolean matchExpired = true; 774 775 this.readLock.lock(); 776 try { 777 // if has role, check if not expired 778 if (matched) { 779 matchExpired = now - this.matchedList.get(key) > expiration; 780 } 781 return !(matched && !matchExpired); 782 } finally { 783 this.readLock.unlock(); 784 } 785 } 786 787 private boolean unMatchedExpired(final String key, final long now) { 788 final boolean unMatched = this.unMatchedList.containsKey(key); 789 boolean unMatchedExpired = true; 790 791 this.readLock.lock(); 792 try { 793 // check if we know it's missing and we've checked recently 794 if (unMatched) { 795 unMatchedExpired = now - this.unMatchedList.get(key) > expiration; 796 } 797 return !(unMatched && !unMatchedExpired); 798 } finally { 799 this.readLock.unlock(); 800 } 801 } 802 803 // pre-condition is that caller has write lock 804 private void loadPolicies(final Properties props, final Properties policies) { 805 for (int i=0; i<=MAX_NUM_FILTERS; i++) { 806 final int idx = i+1; 807 final String filter = policies.getProperty(PREFIX_FILTER + idx 808 , props.getProperty(PREFIX_FILTER + idx, "")).trim(); 809 if (MAX_NUM_FILTERS == i) { 810 final String errorMessage = "Over the max number of filters allowed: " + i; 811 LOGGER.severe(errorMessage); 812 throw new IllegalArgumentException(errorMessage); 813 } else if (filter.isEmpty()) { 814 break; 815 } 816 this.policy.add(filter); 817 } 818 819 // need minimum of one policy to execute 820 if (0 == this.policy.size()) { 821 final String errorMessage = "Must specify at least one spnego.authz.ldap.filter.1"; 822 LOGGER.severe(errorMessage); 823 throw new IllegalStateException(errorMessage); 824 } 825 } 826 827 // pre-condition is that caller has write lock 828 private void loadResourceNames(final Properties props, final Properties policies) { 829 this.resources = new HashMap<String, Map<String, String[]>>(); 830 for (int i=0; i<=MAX_NUM_FILTERS; i++) { 831 final int idx = i+1; 832 final Map<String, String[]> access = new HashMap<String, String[]>(); 833 834 final String resname = policies.getProperty(PREFIX_NAME + idx 835 , props.getProperty(PREFIX_NAME + idx, "")).trim(); 836 837 final String restype = policies.getProperty(PREFIX_TYPE + idx 838 , props.getProperty(PREFIX_TYPE + idx, "").toLowerCase().trim()); 839 840 final String[] resaccess = policies.getProperty(PREFIX_ACCESS + idx 841 , props.getProperty(PREFIX_ACCESS + idx, "")).trim().split(","); 842 843 for (int j=0; j<resaccess.length; j++) { 844 resaccess[j] = resaccess[j].trim(); 845 } 846 847 access.put(restype, resaccess); 848 849 if (MAX_NUM_FILTERS == i) { 850 final String errorMessage = "Over the max number of resources allowed: " + i; 851 LOGGER.severe(errorMessage); 852 throw new IllegalArgumentException(errorMessage); 853 } else if (resname.isEmpty()) { 854 break; 855 } 856 857 this.resources.put(resname, access); 858 } 859 } 860 861 // pre-condition is that caller has write lock 862 private UserInfo cacheUserInfo(final String username) throws NamingException { 863 864 if (null == this.userInfoFilter 865 || this.userInfoFilter.isEmpty() 866 || this.userInfoLabels.size() == 0) { 867 LOGGER.info(USER_INFO_FILTER + " was empty OR no value(s) specified for the " 868 + USER_INFO +" property"); 869 return null; 870 } 871 872 // perform AD lookup add to cache 873 final LdapContext context = new InitialLdapContext(this.environment, null); 874 final NamingEnumeration<SearchResult> results = 875 context.search(this.deecee 876 , String.format(this.userInfoFilter, username) 877 , this.srchCntrls); 878 879 boolean found = false; 880 final Map<String, List<String>> labelInfo = new HashMap<String, List<String>>(); 881 while (results.hasMoreElements()) { 882 found = true; 883 final SearchResult result = (SearchResult) results.nextElement(); 884 final Attributes attributes = result.getAttributes(); 885 for (@SuppressWarnings("rawtypes") 886 NamingEnumeration iter = attributes.getAll(); iter.hasMore();) { 887 final Attribute attribute = (Attribute) iter.next(); 888 final String label = attribute.getID(); 889 final List<String> info = new ArrayList<String>(); 890 if (this.userInfoLabels.contains(label)) { 891 labelInfo.put(label, info); 892 for (@SuppressWarnings("rawtypes") 893 NamingEnumeration enmr = attribute.getAll(); enmr.hasMore();) { 894 info.add(enmr.next().toString()); 895 } 896 } 897 } 898 } 899 results.close(); 900 context.close(); 901 902 // add to cache 903 final UserInfo userInfoObject; 904 if (found) { 905 //LOGGER.info("add to cache userInfoList"); 906 userInfoObject = new UserInfo() { 907 private final Map<String, List<String>> info = labelInfo; 908 private final String labels = userInfoLabels.toString(); 909 910 @Override 911 public List<String> getInfo(final String label) { 912 if (!hasInfo(label)) { 913 throw new NullPointerException( 914 "UserInfo label not found or not in user store: " + label 915 + " - labels specified in property file: " + labels); 916 } 917 return new ArrayList<String>(info.get(label)); 918 } 919 920 @Override 921 public List<String> getLabels() { 922 return new ArrayList<String>(info.keySet()); 923 } 924 925 @Override 926 public boolean hasInfo(final String label) { 927 return info.containsKey(label); 928 } 929 }; 930 this.userInfoList.put(username, userInfoObject); 931 } else { 932 throw new IllegalArgumentException("UserInfo not found. " 933 + ". This MAY be caused by an incorrect spnego.authz.ldap.user.filter definition" 934 + "; filter=" + this.userInfoFilter 935 + "; policy=" + this.policy); 936 } 937 938 return userInfoObject; 939 } 940}