1 /* Copyright (c) 2001-2016, The HSQL Development Group 2 * All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are met: 6 * 7 * Redistributions of source code must retain the above copyright notice, this 8 * list of conditions and the following disclaimer. 9 * 10 * Redistributions in binary form must reproduce the above copyright notice, 11 * this list of conditions and the following disclaimer in the documentation 12 * and/or other materials provided with the distribution. 13 * 14 * Neither the name of the HSQL Development Group nor the names of its 15 * contributors may be used to endorse or promote products derived from this 16 * software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG, 22 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 23 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 24 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 32 package org.hsqldb.auth; 33 34 import java.io.IOException; 35 import java.util.ArrayList; 36 import java.util.Hashtable; 37 import java.util.List; 38 import java.util.regex.Matcher; 39 import java.util.regex.Pattern; 40 import javax.naming.AuthenticationException; 41 import javax.naming.Context; 42 import javax.naming.NamingEnumeration; 43 import javax.naming.NamingException; 44 import javax.naming.directory.Attribute; 45 import javax.naming.directory.Attributes; 46 import javax.naming.directory.BasicAttributes; 47 import javax.naming.directory.SearchResult; 48 import javax.naming.ldap.InitialLdapContext; 49 import javax.naming.ldap.LdapContext; 50 import javax.naming.ldap.StartTlsRequest; 51 import javax.naming.ldap.StartTlsResponse; 52 53 import org.hsqldb.lib.FrameworkLogger; 54 55 /** 56 * Authenticates to a HyperSQL catalog according to entries in a LDAP 57 * database. 58 * If using LDAP StartTLS and your server has a certificate not trusted by 59 * default by your JRE, then set system property 'javax.net.ssl.trustStore' to 60 * the path to a trust store containing the cert (as well as any other certs 61 * that your app needs for other purposes). 62 * <P> 63 * This class with authenticate login attempts against LDAP entries with RDN of 64 * the HyperSQL account name (the precise attribute name defaults to 'uid', but 65 * you may change that). 66 * </P> <P> 67 * This class purposefully does not support LDAPS, because LDAPS is deprecated 68 * in favor of StartTLS, which we do support. 69 * If you need to support LDAPS and are using SE 1.6, use our JaasAuthBean with 70 * Sun's LdapLoginModule. 71 * </P> <P> 72 * This class does not support SASL/External authentication, because the work 73 * involved with securely obtaining user-specific certs would be more complex 74 * than everything else here combined. 75 * Another AuthFunctionBean would have to be written if SASL/External is needed. 76 * </P> <P> 77 * To use instances of this class, you must use at least the methods 78 * setLdapHost, setParentDn, initialize, plus 79 * rolesSchemaAttribute and/or accessAttribute. 80 * </P> <P> 81 * For a user to be given HyperSQL catalog access, that user must either have 82 * a value for accessAttribute if that property is set (optionally requiring 83 * a match with accessValuePattern); or, if the accessAttribute is not set then 84 * must have some (any) value for rolesSchemaAttribute (optionally requiring a 85 * match with roleSchemaValuePattern). 86 * Consequently, if you have set both accessAttribute and rolesSchemaAttribute, 87 * the latter attribute will only be consulted if the check of the former 88 * attribute succeeds. 89 * </P> <P> 90 * If you want roles assigned according to the local HyperSQL database instead 91 * of according to LDAP, then set accessAttribute but not rolesSchemaAttribute. 92 * </P> <P> 93 * If what is wanted is to grant access but with no roles (overriding local 94 * roles if there are any), then set both accessAttribute and 95 * rolesSchemaAttribute, but do not set any rolesSchemaAttribute attribute 96 * values for these no-role users. 97 * (I hesitate to mention it, but you could accomplish the same thing with only 98 * a rolesSchemaAttribute attribute, by setting only a dummy role/schema value 99 * for non-role users, because HyperSQL will ignore unknown roles or schemas 100 * but still give access since a list was still supplied). 101 * </P> 102 * 103 * @see AuthFunctionBean 104 * @see #setLdapHost(String) 105 * @see #setParentDn(String) 106 * @see #init() 107 * @author Blaine Simpson (blaine dot simpson at admc dot com) 108 * @since 2.0.1 109 */ 110 public class LdapAuthBean implements AuthFunctionBean { 111 private static FrameworkLogger logger = 112 FrameworkLogger.getLog(LdapAuthBean.class); 113 114 private Integer ldapPort; 115 private String ldapHost, principalTemplate, saslRealm, parentDn; 116 private Pattern roleSchemaValuePattern, accessValuePattern; 117 private String initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory"; 118 private boolean tls; // This is for StartTLS, not tunneled TLS/LDAPS. 119 // Variable named just "tls" only for brevity. 120 private String mechanism = "SIMPLE"; 121 private String rdnAttribute = "uid"; 122 private boolean initialized; 123 private String rolesSchemaAttribute, accessAttribute; 124 protected String[] attributeUnion; 125 LdapAuthBean()126 public LdapAuthBean() { 127 // Intentionally empty 128 } 129 130 /** 131 * If this is set, then the entire (brief) transaction with the LDAP server 132 * will be encrypted. 133 */ setStartTls(boolean isTls)134 public void setStartTls(boolean isTls) { 135 this.tls = isTls; 136 } 137 setLdapPort(int ldapPort)138 public void setLdapPort(int ldapPort) { 139 this.ldapPort = Integer.valueOf(ldapPort); 140 } 141 142 /** 143 * @throws IllegalStateException if any required setting has not been set. 144 */ init()145 public void init() { 146 if (ldapHost == null) { 147 throw new IllegalStateException( 148 "Required property 'ldapHost' not set"); 149 } 150 if (parentDn == null) { 151 throw new IllegalStateException( 152 "Required property 'parentDn' not set"); 153 } 154 if (initialContextFactory == null) { 155 throw new IllegalStateException( 156 "Required property 'initialContextFactory' not set"); 157 } 158 if (mechanism == null) { 159 throw new IllegalStateException( 160 "Required property 'mechanism' not set"); 161 } 162 if (rdnAttribute == null) { 163 throw new IllegalStateException( 164 "Required property 'rdnAttribute' not set"); 165 } 166 if (rolesSchemaAttribute == null && accessAttribute == null) { 167 throw new IllegalStateException( 168 "You must set property 'rolesSchemaAttribute' " 169 + "and/or property 'accessAttribute'"); 170 } 171 if (roleSchemaValuePattern != null && rolesSchemaAttribute == null) { 172 throw new IllegalStateException( 173 "If property 'roleSchemaValuePattern' is set, then you " 174 + "must also set property 'rolesSchemaAttribute' to " 175 + "indicate which attribute to evaluate"); 176 } 177 if (accessValuePattern != null && accessAttribute == null) { 178 throw new IllegalStateException( 179 "If property 'accessValuePattern' is set, then you " 180 + "must also set property 'accessAttribute' to " 181 + "indicate which attribute to evaluate"); 182 } 183 if (rolesSchemaAttribute != null && accessAttribute != null) { 184 attributeUnion = new String[] 185 { rolesSchemaAttribute, accessAttribute }; 186 } else if (rolesSchemaAttribute != null) { 187 attributeUnion = new String[] { rolesSchemaAttribute }; 188 } else { 189 attributeUnion = new String[] { accessAttribute }; 190 } 191 initialized = true; 192 } 193 194 /** 195 * Assign a pattern to detect honored accessAttribute values. 196 * If you set accessAttribute but not accessValuePattern, then all that will 197 * be checked for access is if the RDN + parentDN entry has the 198 * accessAttribute attribute. (I.e. the specific value will not matter 199 * whatsoever). 200 * </P><P> 201 * You may only use this property if you have set property accessAttribute. 202 * If you have set accessAttribute but not this property, then access will 203 * be decided based solely upon existence of this attribute. 204 * </P><P> 205 * Capture groups in the pattern will be ignored and serve no purpose. 206 * </P><P> 207 * N.b. this Pattern will be used for the matches() operation, therefore it 208 * must match the entire candidate value strings (this is different than 209 * the find operation which does not need to satisfy the entire candidate 210 * value). 211 * </P><P>Example1 :<CODE><PRE> 212 * TRUE 213 * </PRE></CODE> 214 * This will match true values per OpenLDAP's boolean OID. 215 * </P> 216 * 217 * @see Matcher#matches() 218 */ setAccessValuePattern(Pattern accessValuePattern)219 public void setAccessValuePattern(Pattern accessValuePattern) { 220 this.accessValuePattern = accessValuePattern; 221 } 222 223 /** 224 * String wrapper for method setAccessValuePattern(Pattern) 225 * 226 * Use the (x?) Pattern constructs to set options. 227 * 228 * @throws java.util.regex.PatternSyntaxException 229 * @see #setAccessValuePattern(Pattern) 230 */ setAccessValuePatternString(String patternString)231 public void setAccessValuePatternString(String patternString) { 232 setAccessValuePattern(Pattern.compile(patternString)); 233 } 234 235 /** 236 * Assign a pattern to both detect honored values, and to map from a single 237 * value of "rolesSchemaAttribute"s to a HyperSQL role or schema string. 238 * If your rolesSchemaAttribute holds only the String values precisely as 239 * HyperSQL needs them, then don't use this method at all and all matching 240 * attribute values will be passed directly. 241 * </P><P> 242 * You may only use this property if you have set property 243 * rolesSchemaAttribute. 244 * If rolesSchemaAttribute is set but this property is not set, then 245 * the value will directly determine the user's roles and schema. 246 * </P><P> 247 * <B>Unlike the rolesSchemaAttribute, the property at-hand uses the 248 * singular for "role", because whereas rolesSchemaAttribute is the 249 * attribute for listing multiple roles, roleSchemaValuePattern is used 250 * to evaluate single role values.</B> 251 * </P><P> 252 * These are two distinct and important purposes for the specified Pattern. 253 * <OL> 254 * <LI> 255 * Values that do not successfully match the pattern will be ignored. 256 * <LI> 257 * Optionally uses parentheses to specify a single capture group 258 * (if you use parentheses to specify more than one matching group, we 259 * will only capture for the first). 260 * What is captured by this group is exactly the role or schema that 261 * HyperSQL will attempt to assign. 262 * If no capture parens are given then the Pattern is only used for the 263 * acceptance decision, and the LDAP-provided value will be returned 264 * verbatim. 265 * </OL> 266 * </P><P> 267 * Together, these two features work great to extract just the needed role 268 * and schema names from 'memberof' DNs, and will have no problem if you 269 * also use 'memberof' for unrelated purposes. 270 * </P><P> 271 * N.b. this Pattern will be used for the matches() operation, therefore it 272 * must match the entire candidate value strings (this is different than 273 * the find operation which does not need to satisfy the entire candidate 274 * value). 275 * </P><P>Example1 :<CODE><PRE> 276 * cn=([^,]+),ou=dbRole,dc=admc,dc=com 277 * </PRE></CODE> 278 * will extract the CN value from matching attribute values. 279 * </P><P>Example1 :<CODE><PRE> 280 * cn=[^,]+,ou=dbRole,dc=admc,dc=com 281 * </PRE></CODE> 282 * will return the entire <CODE>cn...com</CODE> string for matching 283 * attribute values. 284 * </P> 285 * 286 * @see Matcher#matches() 287 */ setRoleSchemaValuePattern(Pattern roleSchemaValuePattern)288 public void setRoleSchemaValuePattern(Pattern roleSchemaValuePattern) { 289 this.roleSchemaValuePattern = roleSchemaValuePattern; 290 } 291 292 /** 293 * String wrapper for method setRoleSchemaValuePattern(Pattern) 294 * 295 * Use the (x?) Pattern constructs to set options. 296 * 297 * @throws java.util.regex.PatternSyntaxException 298 * @see #setRoleSchemaValuePattern(Pattern) 299 */ setRoleSchemaValuePatternString(String patternString)300 public void setRoleSchemaValuePatternString(String patternString) { 301 setRoleSchemaValuePattern(Pattern.compile(patternString)); 302 } 303 304 /** 305 * Defaults to "SIMPLE". 306 * 307 * @param mechanism Either 'SIMPLE' (the default) for LDAP Simple, or 308 * one of the LDAP SASL mechanisms, such as 'DIGEST-MD5'. 309 */ setSecurityMechanism(String mechanism)310 public void setSecurityMechanism(String mechanism) { 311 this.mechanism = mechanism; 312 } 313 314 /** 315 * Do not specify URL scheme ("ldap:") because that is implied. 316 * (Since we purposefully don't support LDAPS, there would be no reason to 317 * change that). 318 * <P> 319 * If using StartTLS, then this host name must match the cn of the LDAP 320 * server's certificate. 321 * </P> <P> 322 * If you need to support LDAPS and are using SE 1.6, use our JaasAuthBean 323 * with Sun's LdapLoginModule instead of this class. 324 * </P> 325 * 326 * @see JaasAuthBean 327 */ setLdapHost(String ldapHost)328 public void setLdapHost(String ldapHost) { 329 this.ldapHost = ldapHost; 330 } 331 332 /** 333 * A template String containing place-holder token '${username}'. 334 * All occurrences of '${username}' (without the quotes) will be translated 335 * to the username that authentication is being attempted with. 336 * <P> 337 * If you supply a principalTemplate that does not contain '${username}', 338 * then authentication will be user-independent. 339 * </P> <P> 340 * It is common to authenticate to LDAP servers with the DN of the user's 341 * LDAP entry. In this situation, set principalTemplate to 342 * <CODE><RDN_ATTR=>${username},<PARENT_DN></CODE>. 343 * For example if you use parentDn of 344 * <CODE>"ou=people,dc=admc,dc=com"</CODE> and rdnAttribute of 345 * <CODE>uid</CODE>, then you would set <CODE><PRE> 346 * "uid=${username},ou=people,dc=admc,dc=com" 347 * </PRE></CODE> 348 * </P> <P> 349 * By default the user name will be passed exactly as it is, so don't use 350 * this setter if that is what you want. (This works great for OpenLDAP 351 * with DIGEST-MD5 SASL, for example). 352 * </P> 353 */ setPrincipalTemplate(String principalTemplate)354 public void setPrincipalTemplate(String principalTemplate) { 355 this.principalTemplate = principalTemplate; 356 } 357 358 /** 359 * Most users should not call this, and will get the default of 360 * "com.sun.jndi.ldap.LdapCtxFactory". 361 * Use this method if you prefer to use a context factory provided by your 362 * framework or container, for example, or if you are using a non-Sun JRE. 363 */ setInitialContextFactory(String initialContextFactory)364 public void setInitialContextFactory(String initialContextFactory) { 365 this.initialContextFactory = initialContextFactory; 366 } 367 368 /** 369 * Some LDAP servers using a SASL mechanism require a realm to be specified, 370 * and some mechanisms allow a realm to be specified if you wish to use that 371 * feature. 372 * By default no realm will be sent to the LDAP server. 373 * <P> 374 * Don't use this setter if you are not setting a SASL mechanism. 375 * </P> 376 */ setSaslRealm(String saslRealm)377 public void setSaslRealm(String saslRealm) { 378 this.saslRealm = saslRealm; 379 } 380 381 /** 382 * Set DN which is parent of the user DNs. 383 * E.g. "ou=people,dc=admc,dc=com" 384 */ setParentDn(String parentDn)385 public void setParentDn(String parentDn) { 386 this.parentDn = parentDn; 387 } 388 389 /** 390 * rdnAttribute must hold the user name exactly as the HyperSQL login will 391 * be made with. 392 * <P> 393 * This is the RDN relative to the Parent DN specified with setParentDN. 394 * Defaults to 'uid'. 395 * </P> 396 * 397 * @see #setParentDn(String) 398 */ setRdnAttribute(String rdnAttribute)399 public void setRdnAttribute(String rdnAttribute) { 400 this.rdnAttribute = rdnAttribute; 401 } 402 403 /** 404 * Set the attribute name of the RDN + parentDn entries in which is stored 405 * the list of roles and optional schema for the authenticating user. 406 * <P> 407 * There is no default. <b>You must set this attribute if you want LDAP 408 * instead of the local HyperSQL database to determine the user's roles!</b> 409 * You must set the rolesSchemaAttribute property and/or the 410 * accessAttribute property. 411 * Consequently, if you do no tset this property, then you must set the 412 * accessAttribute property, and this LdapAuthBean will only determine 413 * access not roles. 414 * </P> <P> 415 * To use the nice <i>reverse group membership</i> feature of LDAP, set 416 * this value to "memberof". 417 * </P> <P> 418 * If you have set both rolesSchemaAttribute and this value, then the 419 * attribute set here will only be consulted if the accessAttribute check 420 * succeeds. 421 * </P> 422 */ setRolesSchemaAttribute(String attribute)423 public void setRolesSchemaAttribute(String attribute) { 424 rolesSchemaAttribute = attribute; 425 } 426 427 /** 428 * Set the attribute name of the RDN + parentDn entries which will be 429 * consulted to decide whether the user can access the HyperSQL database. 430 * <P> 431 * There is no default. If you set this attribute, then the attribute will 432 * determine whether the user can access the HyperSQL database, regardless 433 * of whether the rolesSchemaAttribute attribute is set. 434 * </P> <P> 435 * If you set just this property, then the local HyperSQL database will 436 * decide all roles for the user. If you set this property and property 437 * rolesSchemaAttribute then this attribute will determine access, and if 438 * this attribute grants access then the rolesSchemaAttribute value will 439 * determine the user's roles. 440 * </P> 441 */ setAccessAttribute(String attribute)442 public void setAccessAttribute(String attribute) { 443 accessAttribute = attribute; 444 } 445 446 /** 447 * @see AuthFunctionBean#authenticate(String, String) 448 */ authenticate(String userName, String password)449 public String[] authenticate(String userName, String password) 450 throws DenyException { 451 if (!initialized) { 452 throw new IllegalStateException( 453 "You must invoke the 'init' method to initialize the " 454 + LdapAuthBean.class.getName() + " instance."); 455 } 456 Hashtable env = new Hashtable(5, 0.75f); 457 env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory); 458 env.put(Context.PROVIDER_URL, "ldap://" + ldapHost 459 + ((ldapPort == null) ? "" : (":" + ldapPort))); 460 StartTlsResponse tlsResponse = null; 461 LdapContext ctx = null; 462 463 try { 464 ctx = new InitialLdapContext(env, null); 465 466 if (tls) { 467 // Requesting to start TLS on an LDAP association 468 tlsResponse = (StartTlsResponse) ctx.extendedOperation( 469 new StartTlsRequest()); 470 471 // Starting TLS 472 tlsResponse.negotiate(); 473 } 474 475 // A TLS/SSL secure channel has been established if you reach here. 476 477 // Assertion of client's authorization Identity -- Explicit way 478 ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, mechanism); 479 ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, 480 ((principalTemplate == null) 481 ? userName 482 : principalTemplate.replace("${username}", userName))); 483 ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password); 484 if (saslRealm != null) { 485 env.put("java.naming.security.sasl.realm", saslRealm); 486 } 487 488 // The Context.SECURITY_* authorizations are only applied when the 489 // following statement executes. (Or any other remote operations done 490 // while the TLS connection is still open). 491 NamingEnumeration<SearchResult> sRess = null; 492 try { 493 sRess = ctx.search(parentDn, 494 new BasicAttributes(rdnAttribute, userName), 495 attributeUnion); 496 } catch (AuthenticationException ae) { 497 throw new DenyException(); 498 } catch (Exception e) { 499 throw new RuntimeException(e); 500 } 501 if (!sRess.hasMore()) { 502 throw new DenyException(); 503 } 504 SearchResult sRes = sRess.next(); 505 if (sRess.hasMore()) { 506 throw new RuntimeException("> 1 result"); 507 } 508 Attributes attrs = sRes.getAttributes(); 509 if (accessAttribute != null) { 510 Attribute attribute = attrs.get(accessAttribute); 511 if (attribute == null) { 512 throw new DenyException(); 513 } 514 if (attribute.size() != 1) { 515 throw new RuntimeException("Access attribute '" 516 + accessAttribute + "' has unexpected value count: " 517 + attribute.size()); 518 } 519 if (accessValuePattern != null) { 520 Object accessValue = attribute.get(0); 521 if (accessValue == null) { 522 throw new RuntimeException( 523 "Access Attr. value is null"); 524 } 525 if (!(accessValue instanceof String)) { 526 throw new RuntimeException("Access Attr. value " 527 + "not a String: " 528 + accessValue.getClass().getName()); 529 } 530 if (!accessValuePattern.matcher( 531 (String) accessValue).matches()) { 532 throw new DenyException(); 533 } 534 } 535 } 536 if (rolesSchemaAttribute == null) { 537 return null; 538 } 539 540 // If we reach here, then we definitely need to try to return a 541 // list of roles + schema. 542 List<String> returns = new ArrayList<String>(); 543 Attribute attribute = attrs.get(rolesSchemaAttribute); 544 if (attribute != null) { 545 int valCount = attribute.size(); 546 Matcher matcher; 547 Object oneVal; 548 for (int i = 0; i < valCount; i++) { 549 oneVal = attribute.get(i); 550 if (oneVal == null) { 551 throw new RuntimeException( 552 "R/S Attr value #" + i + " is null"); 553 } 554 if (!(oneVal instanceof String)) { 555 throw new RuntimeException( 556 "R/S Attr value #" + i + " not a String: " 557 + oneVal.getClass().getName()); 558 } 559 if (roleSchemaValuePattern == null) { 560 returns.add((String) oneVal); 561 } else { 562 matcher = roleSchemaValuePattern.matcher( 563 (String) oneVal); 564 if (matcher.matches()) { 565 returns.add((matcher.groupCount() > 0) 566 ? matcher.group(1) 567 : (String) oneVal); 568 } 569 } 570 } 571 } 572 if (returns.size() < 1) { 573 if (accessAttribute == null) { 574 throw new DenyException(); 575 } 576 return new String[0]; 577 } 578 return returns.toArray(new String[0]); 579 } catch (DenyException de) { 580 // This throws a non-runtime Exception, which is handled as an 581 // access denial instead of a system problem. 582 throw de; 583 } catch (RuntimeException re) { 584 throw re; 585 } catch (IOException ioe) { 586 throw new RuntimeException(ioe); 587 } catch (NamingException ne) { 588 throw new RuntimeException(ne); 589 } finally { 590 if (tlsResponse != null) try { 591 tlsResponse.close(); 592 } catch (IOException ioe) { 593 logger.error("Failed to close TLS Response", ioe); 594 } 595 if (ctx != null) try { 596 ctx.close(); 597 } catch (NamingException ne) { 598 logger.error("Failed to close LDAP Context", ne); 599 } 600 } 601 } 602 } 603