Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
SUSE:SLE-15-SP2:Update
tomcat.23183
tomcat-9.0-CVE-2021-30640.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File tomcat-9.0-CVE-2021-30640.patch of Package tomcat.23183
Index: apache-tomcat-9.0.36-src/build.properties.default =================================================================== --- apache-tomcat-9.0.36-src.orig/build.properties.default +++ apache-tomcat-9.0.36-src/build.properties.default @@ -248,6 +248,15 @@ objenesis.home=${base.path}/objenesis-${ objenesis.jar=${objenesis.home}/objenesis-${objenesis.version}.jar objenesis.loc=${base-maven.loc}/org/objenesis/objenesis/${objenesis.version}/objenesis-${objenesis.version}.jar +# ----- UnboundID, used by unit tests, version 5.1.4 or later ----- +unboundid.version=5.1.4 +unboundid.checksum.enabled=true +unboundid.checksum.algorithm=SHA-512 +unboundid.checksum.value=04cf7f59eddebdd5b51e5be55021f9d9c667cca6101eac954e7a8d5b51f4c23372cd8f041640157f082435a166b75d85e79252b516130ede7d966dae6d3eae67 +unboundid.home=${base.path}/unboundid-${unboundid.version} +unboundid.jar=${unboundid.home}/unboundid-ldapsdk-${unboundid.version}.jar +unboundid.loc=${base-maven.loc}/com/unboundid/unboundid-ldapsdk/${unboundid.version}/unboundid-ldapsdk-${unboundid.version}.jar + # ----- Checkstyle, version 6.16 or later ----- checkstyle.version=8.22 checkstyle.checksum.enabled=true Index: apache-tomcat-9.0.36-src/build.xml =================================================================== --- apache-tomcat-9.0.36-src.orig/build.xml +++ apache-tomcat-9.0.36-src/build.xml @@ -2862,6 +2862,15 @@ skip.installer property in build.propert <param name="checksum.value" value="${objenesis.checksum.value}"/> </antcall> + <antcall target="downloadfile"> + <param name="sourcefile" value="${unboundid.loc}"/> + <param name="destfile" value="${unboundid.jar}"/> + <param name="destdir" value="${unboundid.home}"/> + <param name="checksum.enabled" value="${unboundid.checksum.enabled}"/> + <param name="checksum.algorithm" value="${unboundid.checksum.algorithm}"/> + <param name="checksum.value" value="${unboundid.checksum.value}"/> + </antcall> + </target> <target name="download-cobertura" Index: apache-tomcat-9.0.36-src/java/org/apache/catalina/realm/JNDIRealm.java =================================================================== --- apache-tomcat-9.0.36-src.orig/java/org/apache/catalina/realm/JNDIRealm.java +++ apache-tomcat-9.0.36-src/java/org/apache/catalina/realm/JNDIRealm.java @@ -32,6 +32,8 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import javax.naming.AuthenticationException; import javax.naming.CommunicationException; @@ -61,6 +63,7 @@ import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; import org.apache.catalina.LifecycleException; +import org.apache.tomcat.util.collections.SynchronizedStack; import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSName; @@ -166,10 +169,6 @@ import org.ietf.jgss.GSSName; * directory server itself.</li> * </ul> * - * <p><strong>TODO</strong> - Support connection pooling (including message - * format objects) so that <code>authenticate()</code> does not have to be - * synchronized.</p> - * * <p><strong>WARNING</strong> - There is a reported bug against the Netscape * provider code (com.netscape.jndi.ldap.LdapContextFactory) with respect to * successfully authenticated a non-existing user. The @@ -182,7 +181,6 @@ import org.ietf.jgss.GSSName; */ public class JNDIRealm extends RealmBase { - // ----------------------------------------------------- Instance Variables /** @@ -195,25 +193,16 @@ public class JNDIRealm extends RealmBase */ protected String connectionName = null; - /** * The connection password for the server we will contact. */ protected String connectionPassword = null; - /** * The connection URL for the server we will contact. */ protected String connectionURL = null; - - /** - * The directory context linking us to our directory server. - */ - protected DirContext context = null; - - /** * The JNDI context factory used to acquire our InitialContext. By * default, assumes use of an LDAP server using the standard JNDI LDAP @@ -221,7 +210,6 @@ public class JNDIRealm extends RealmBase */ protected String contextFactory = "com.sun.jndi.ldap.LdapCtxFactory"; - /** * How aliases should be dereferenced during search operations. */ @@ -233,14 +221,12 @@ public class JNDIRealm extends RealmBase */ public static final String DEREF_ALIASES = "java.naming.ldap.derefAliases"; - /** * The protocol that will be used in the communication with the * directory server. */ protected String protocol = null; - /** * Should we ignore PartialResultExceptions when iterating over NamingEnumerations? * Microsoft Active Directory often returns referrals, which lead @@ -250,7 +236,6 @@ public class JNDIRealm extends RealmBase */ protected boolean adCompat = false; - /** * How should we handle referrals? Microsoft Active Directory often returns * referrals. If you need to follow them set referrals to "follow". @@ -259,20 +244,17 @@ public class JNDIRealm extends RealmBase */ protected String referrals = null; - /** * The base element for user searches. */ protected String userBase = ""; - /** * The message format used to search for a user, with "{0}" marking * the spot where the username goes. */ protected String userSearch = null; - /** * When searching for users, should the search be performed as the user * currently being authenticated? If false, {@link #connectionName} and @@ -281,20 +263,11 @@ public class JNDIRealm extends RealmBase */ private boolean userSearchAsUser = false; - - /** - * The MessageFormat object associated with the current - * <code>userSearch</code>. - */ - protected MessageFormat userSearchFormat = null; - - /** * Should we search the entire subtree for matching users? */ protected boolean userSubtree = false; - /** * The attribute name used to retrieve the user password. */ @@ -308,7 +281,6 @@ public class JNDIRealm extends RealmBase */ protected String userRoleAttribute = null; - /** * A string of LDAP user patterns or paths, ":"-separated * These will be used to form the distinguished name of a @@ -319,7 +291,6 @@ public class JNDIRealm extends RealmBase */ protected String[] userPatternArray = null; - /** * The message format used to form the distinguished name of a * user, with "{0}" marking the spot where the specified username @@ -327,46 +298,22 @@ public class JNDIRealm extends RealmBase */ protected String userPattern = null; - - /** - * An array of MessageFormat objects associated with the current - * <code>userPatternArray</code>. - */ - protected MessageFormat[] userPatternFormatArray = null; - /** * The base element for role searches. */ protected String roleBase = ""; - - /** - * The MessageFormat object associated with the current - * <code>roleBase</code>. - */ - protected MessageFormat roleBaseFormat = null; - - - /** - * The MessageFormat object associated with the current - * <code>roleSearch</code>. - */ - protected MessageFormat roleFormat = null; - - /** * The name of an attribute in the user's entry containing * roles for that user */ protected String userRoleName = null; - /** * The name of the attribute containing roles held elsewhere */ protected String roleName = null; - /** * The message format used to select roles for a user, with "{0}" marking * the spot where the distinguished name of the user goes. The "{1}" @@ -374,7 +321,6 @@ public class JNDIRealm extends RealmBase */ protected String roleSearch = null; - /** * Should we search the entire subtree for matching memberships? */ @@ -409,7 +355,6 @@ public class JNDIRealm extends RealmBase */ protected String commonRole = null; - /** * The timeout, in milliseconds, to use when trying to create a connection * to the directory. The default is 5000 (5 seconds). @@ -434,14 +379,12 @@ public class JNDIRealm extends RealmBase */ protected int timeLimit = 0; - /** * Should delegated credentials from the SPNEGO authenticator be used if * available */ protected boolean useDelegatedCredential = true; - /** * The QOP that should be used for the connection to the LDAP server after * authentication. This value is used to set the @@ -499,6 +442,33 @@ public class JNDIRealm extends RealmBase private boolean forceDnHexEscape = false; + /** + * Non pooled connection to our directory server. + */ + protected JNDIConnection singleConnection = new JNDIConnection(); + + /** + * The lock to ensure single connection thread safety. + */ + protected final Lock singleConnectionLock = new ReentrantLock(); + + /** + * Connection pool. + */ + protected SynchronizedStack<JNDIConnection> connectionPool = null; + + /** + * The pool size limit. If 1, pooling is not used. + */ + protected int connectionPoolSize = 1; + + /** + * Whether to use context ClassLoader or default ClassLoader. + * True means use context ClassLoader, and True is the default + * value. + */ + protected boolean useContextClassLoader = true; + // ------------------------------------------------------------- Properties @@ -506,37 +476,35 @@ public class JNDIRealm extends RealmBase return forceDnHexEscape; } + public void setForceDnHexEscape(boolean forceDnHexEscape) { this.forceDnHexEscape = forceDnHexEscape; } + /** * @return the type of authentication to use. */ public String getAuthentication() { - return authentication; - } + /** * Set the type of authentication to use. * * @param authentication The authentication */ public void setAuthentication(String authentication) { - this.authentication = authentication; - } + /** * @return the connection username for this Realm. */ public String getConnectionName() { - return this.connectionName; - } @@ -546,9 +514,7 @@ public class JNDIRealm extends RealmBase * @param connectionName The new connection username */ public void setConnectionName(String connectionName) { - this.connectionName = connectionName; - } @@ -556,9 +522,7 @@ public class JNDIRealm extends RealmBase * @return the connection password for this Realm. */ public String getConnectionPassword() { - return this.connectionPassword; - } @@ -568,9 +532,7 @@ public class JNDIRealm extends RealmBase * @param connectionPassword The new connection password */ public void setConnectionPassword(String connectionPassword) { - this.connectionPassword = connectionPassword; - } @@ -578,9 +540,7 @@ public class JNDIRealm extends RealmBase * @return the connection URL for this Realm. */ public String getConnectionURL() { - return this.connectionURL; - } @@ -590,9 +550,7 @@ public class JNDIRealm extends RealmBase * @param connectionURL The new connection URL */ public void setConnectionURL(String connectionURL) { - this.connectionURL = connectionURL; - } @@ -600,9 +558,7 @@ public class JNDIRealm extends RealmBase * @return the JNDI context factory for this Realm. */ public String getContextFactory() { - return this.contextFactory; - } @@ -612,11 +568,10 @@ public class JNDIRealm extends RealmBase * @param contextFactory The new context factory */ public void setContextFactory(String contextFactory) { - this.contextFactory = contextFactory; - } + /** * @return the derefAliases setting to be used. */ @@ -624,33 +579,32 @@ public class JNDIRealm extends RealmBase return derefAliases; } + /** * Set the value for derefAliases to be used when searching the directory. * * @param derefAliases New value of property derefAliases. */ public void setDerefAliases(java.lang.String derefAliases) { - this.derefAliases = derefAliases; + this.derefAliases = derefAliases; } + /** * @return the protocol to be used. */ public String getProtocol() { - return protocol; - } + /** * Set the protocol for this Realm. * * @param protocol The new protocol. */ public void setProtocol(String protocol) { - this.protocol = protocol; - } @@ -694,9 +648,7 @@ public class JNDIRealm extends RealmBase * @return the base element for user searches. */ public String getUserBase() { - return this.userBase; - } @@ -706,9 +658,7 @@ public class JNDIRealm extends RealmBase * @param userBase The new base element */ public void setUserBase(String userBase) { - this.userBase = userBase; - } @@ -716,9 +666,7 @@ public class JNDIRealm extends RealmBase * @return the message format pattern for selecting users in this Realm. */ public String getUserSearch() { - return this.userSearch; - } @@ -728,13 +676,8 @@ public class JNDIRealm extends RealmBase * @param userSearch The new user search pattern */ public void setUserSearch(String userSearch) { - this.userSearch = userSearch; - if (userSearch == null) - userSearchFormat = null; - else - userSearchFormat = new MessageFormat(userSearch); - + singleConnection = create(); } @@ -752,9 +695,7 @@ public class JNDIRealm extends RealmBase * @return the "search subtree for users" flag. */ public boolean getUserSubtree() { - return this.userSubtree; - } @@ -764,9 +705,7 @@ public class JNDIRealm extends RealmBase * @param userSubtree The new search flag */ public void setUserSubtree(boolean userSubtree) { - this.userSubtree = userSubtree; - } @@ -774,7 +713,6 @@ public class JNDIRealm extends RealmBase * @return the user role name attribute name for this Realm. */ public String getUserRoleName() { - return userRoleName; } @@ -785,9 +723,7 @@ public class JNDIRealm extends RealmBase * @param userRoleName The new userRole name attribute name */ public void setUserRoleName(String userRoleName) { - this.userRoleName = userRoleName; - } @@ -795,9 +731,7 @@ public class JNDIRealm extends RealmBase * @return the base element for role searches. */ public String getRoleBase() { - return this.roleBase; - } @@ -807,13 +741,8 @@ public class JNDIRealm extends RealmBase * @param roleBase The new base element */ public void setRoleBase(String roleBase) { - this.roleBase = roleBase; - if (roleBase == null) - roleBaseFormat = null; - else - roleBaseFormat = new MessageFormat(roleBase); - + singleConnection = create(); } @@ -821,9 +750,7 @@ public class JNDIRealm extends RealmBase * @return the role name attribute name for this Realm. */ public String getRoleName() { - return this.roleName; - } @@ -833,9 +760,7 @@ public class JNDIRealm extends RealmBase * @param roleName The new role name attribute name */ public void setRoleName(String roleName) { - this.roleName = roleName; - } @@ -843,9 +768,7 @@ public class JNDIRealm extends RealmBase * @return the message format pattern for selecting roles in this Realm. */ public String getRoleSearch() { - return this.roleSearch; - } @@ -855,13 +778,8 @@ public class JNDIRealm extends RealmBase * @param roleSearch The new role search pattern */ public void setRoleSearch(String roleSearch) { - this.roleSearch = roleSearch; - if (roleSearch == null) - roleFormat = null; - else - roleFormat = new MessageFormat(roleSearch); - + singleConnection = create(); } @@ -879,9 +797,7 @@ public class JNDIRealm extends RealmBase * @return the "search subtree for roles" flag. */ public boolean getRoleSubtree() { - return this.roleSubtree; - } @@ -891,18 +807,15 @@ public class JNDIRealm extends RealmBase * @param roleSubtree The new search flag */ public void setRoleSubtree(boolean roleSubtree) { - this.roleSubtree = roleSubtree; - } + /** * @return the "The nested group search flag" flag. */ public boolean getRoleNested() { - return this.roleNested; - } @@ -912,9 +825,7 @@ public class JNDIRealm extends RealmBase * @param roleNested The nested group search flag */ public void setRoleNested(boolean roleNested) { - this.roleNested = roleNested; - } @@ -922,9 +833,7 @@ public class JNDIRealm extends RealmBase * @return the password attribute used to retrieve the user password. */ public String getUserPassword() { - return this.userPassword; - } @@ -934,9 +843,7 @@ public class JNDIRealm extends RealmBase * @param userPassword The new password attribute */ public void setUserPassword(String userPassword) { - this.userPassword = userPassword; - } @@ -944,6 +851,7 @@ public class JNDIRealm extends RealmBase return userRoleAttribute; } + public void setUserRoleAttribute(String userRoleAttribute) { this.userRoleAttribute = userRoleAttribute; } @@ -952,14 +860,10 @@ public class JNDIRealm extends RealmBase * @return the message format pattern for selecting users in this Realm. */ public String getUserPattern() { - return this.userPattern; - } - - /** * Set the message format pattern for selecting users in this Realm. * This may be one simple pattern, or multiple patterns to be tried, @@ -971,18 +875,12 @@ public class JNDIRealm extends RealmBase * @param userPattern The new user pattern */ public void setUserPattern(String userPattern) { - this.userPattern = userPattern; - if (userPattern == null) + if (userPattern == null) { userPatternArray = null; - else { + } else { userPatternArray = parseUserPatternString(userPattern); - int len = this.userPatternArray.length; - userPatternFormatArray = new MessageFormat[len]; - for (int i=0; i < len; i++) { - userPatternFormatArray[i] = - new MessageFormat(userPatternArray[i]); - } + singleConnection = create(); } } @@ -993,9 +891,7 @@ public class JNDIRealm extends RealmBase * @return Value of property alternateURL. */ public String getAlternateURL() { - return this.alternateURL; - } @@ -1005,9 +901,7 @@ public class JNDIRealm extends RealmBase * @param alternateURL New value of property alternateURL. */ public void setAlternateURL(String alternateURL) { - this.alternateURL = alternateURL; - } @@ -1015,9 +909,7 @@ public class JNDIRealm extends RealmBase * @return the common role */ public String getCommonRole() { - return commonRole; - } @@ -1027,9 +919,7 @@ public class JNDIRealm extends RealmBase * @param commonRole The common role */ public void setCommonRole(String commonRole) { - this.commonRole = commonRole; - } @@ -1037,9 +927,7 @@ public class JNDIRealm extends RealmBase * @return the connection timeout. */ public String getConnectionTimeout() { - return connectionTimeout; - } @@ -1049,18 +937,15 @@ public class JNDIRealm extends RealmBase * @param timeout The new connection timeout */ public void setConnectionTimeout(String timeout) { - this.connectionTimeout = timeout; - } + /** * @return the read timeout. */ public String getReadTimeout() { - return readTimeout; - } @@ -1070,9 +955,7 @@ public class JNDIRealm extends RealmBase * @param timeout The new read timeout */ public void setReadTimeout(String timeout) { - this.readTimeout = timeout; - } @@ -1100,6 +983,7 @@ public class JNDIRealm extends RealmBase return useDelegatedCredential; } + public void setUseDelegatedCredential(boolean useDelegatedCredential) { this.useDelegatedCredential = useDelegatedCredential; } @@ -1109,6 +993,7 @@ public class JNDIRealm extends RealmBase return spnegoDelegationQop; } + public void setSpnegoDelegationQop(String spnegoDelegationQop) { this.spnegoDelegationQop = spnegoDelegationQop; } @@ -1121,6 +1006,7 @@ public class JNDIRealm extends RealmBase return useStartTls; } + /** * Flag whether StartTLS should be used when connecting to the ldap server * @@ -1132,6 +1018,7 @@ public class JNDIRealm extends RealmBase this.useStartTls = useStartTls; } + /** * @return list of the allowed cipher suites when connections are made using * StartTLS @@ -1151,6 +1038,7 @@ public class JNDIRealm extends RealmBase return this.cipherSuitesArray; } + /** * Set the allowed cipher suites when opening a connection using StartTLS. * The cipher suites are expected as a comma separated list. @@ -1162,6 +1050,25 @@ public class JNDIRealm extends RealmBase this.cipherSuites = suites; } + + /** + * @return the connection pool size, or the default value 1 if pooling + * is disabled + */ + public int getConnectionPoolSize() { + return connectionPoolSize; + } + + + /** + * Set the connection pool size + * @param connectionPoolSize the new pool size + */ + public void setConnectionPoolSize(int connectionPoolSize) { + this.connectionPoolSize = connectionPoolSize; + } + + /** * @return name of the {@link HostnameVerifier} class used for connections * using StartTLS, or the empty string, if the default verifier @@ -1174,6 +1081,7 @@ public class JNDIRealm extends RealmBase return this.hostnameVerifier.getClass().getCanonicalName(); } + /** * Set the {@link HostnameVerifier} to be used when opening connections * using StartTLS. An instance of the given class name will be constructed @@ -1190,6 +1098,7 @@ public class JNDIRealm extends RealmBase } } + /** * @return the {@link HostnameVerifier} to use for peer certificate * verification when opening connections using StartTLS. @@ -1198,8 +1107,7 @@ public class JNDIRealm extends RealmBase if (this.hostnameVerifier != null) { return this.hostnameVerifier; } - if (this.hostNameVerifierClassName == null - || hostNameVerifierClassName.equals("")) { + if (this.hostNameVerifierClassName == null || hostNameVerifierClassName.equals("")) { return null; } try { @@ -1219,6 +1127,7 @@ public class JNDIRealm extends RealmBase } } + /** * Set the {@link SSLSocketFactory} to be used when opening connections * using StartTLS. An instance of the factory with the given name will be @@ -1232,6 +1141,7 @@ public class JNDIRealm extends RealmBase this.sslSocketFactoryClassName = factoryClassName; } + /** * Set the ssl protocol to be used for connections using StartTLS. * @@ -1242,6 +1152,7 @@ public class JNDIRealm extends RealmBase this.sslProtocol = protocol; } + /** * @return the list of supported ssl protocols by the default * {@link SSLContext} @@ -1255,12 +1166,36 @@ public class JNDIRealm extends RealmBase } } + private Object constructInstance(String className) throws ReflectiveOperationException { Class<?> clazz = Class.forName(className); return clazz.getConstructor().newInstance(); } + + /** + * Sets whether to use the context or default ClassLoader. + * True means use context ClassLoader. + * + * @param useContext True means use context ClassLoader + */ + public void setUseContextClassLoader(boolean useContext) { + useContextClassLoader = useContext; + } + + + /** + * Returns whether to use the context or default ClassLoader. + * True means to use the context ClassLoader. + * + * @return The value of useContextClassLoader + */ + public boolean isUseContextClassLoader() { + return useContextClassLoader; + } + + // ---------------------------------------------------------- Realm Methods /** @@ -1280,20 +1215,21 @@ public class JNDIRealm extends RealmBase @Override public Principal authenticate(String username, String credentials) { - DirContext context = null; + JNDIConnection connection = null; Principal principal = null; try { // Ensure that we have a directory context available - context = open(); + connection = get(); - // Occasionally the directory context will timeout. Try one more - // time before giving up. try { + // Occasionally the directory context will timeout. Try one more + // time before giving up. + // Authenticate the specified username if possible - principal = authenticate(context, username, credentials); + principal = authenticate(connection, username, credentials); } catch (NullPointerException | NamingException e) { /* @@ -1315,19 +1251,19 @@ public class JNDIRealm extends RealmBase containerLog.info(sm.getString("jndiRealm.exception.retry"), e); // close the connection so we know it will be reopened. - if (context != null) - close(context); + close(connection); + closePooledConnections(); // open a new directory context. - context = open(); + connection = get(); // Try the authentication again. - principal = authenticate(context, username, credentials); + principal = authenticate(connection, username, credentials); } // Release this context - release(context); + release(connection); // Return the authenticated Principal (if any) return principal; @@ -1337,31 +1273,24 @@ public class JNDIRealm extends RealmBase // Log the problem for posterity containerLog.error(sm.getString("jndiRealm.exception"), e); - // Close the connection so that it gets reopened next time - if (context != null) - close(context); + // close the connection so we know it will be reopened. + close(connection); + closePooledConnections(); // Return "not authenticated" for this request - if (containerLog.isDebugEnabled()) + if (containerLog.isDebugEnabled()) { containerLog.debug("Returning null principal."); + } return null; - } - } - // -------------------------------------------------------- Package Methods - - - // ------------------------------------------------------ Protected Methods - - /** * Return the Principal associated with the specified username and * credentials, if there is one; otherwise return <code>null</code>. * - * @param context The directory context + * @param connection The directory context * @param username Username of the Principal to look up * @param credentials Password or other credentials to use in * authenticating this username @@ -1369,30 +1298,26 @@ public class JNDIRealm extends RealmBase * * @exception NamingException if a directory server error occurs */ - public synchronized Principal authenticate(DirContext context, - String username, - String credentials) - throws NamingException { - - if (username == null || username.equals("") - || credentials == null || credentials.equals("")) { - if (containerLog.isDebugEnabled()) + public Principal authenticate(JNDIConnection connection, String username, String credentials) + throws NamingException { + + if (username == null || username.equals("") || credentials == null || credentials.equals("")) { + if (containerLog.isDebugEnabled()) { containerLog.debug("username null or empty: returning null principal."); + } return null; } if (userPatternArray != null) { - for (int curUserPattern = 0; - curUserPattern < userPatternFormatArray.length; - curUserPattern++) { + for (int curUserPattern = 0; curUserPattern < userPatternArray.length; curUserPattern++) { // Retrieve user information - User user = getUser(context, username, credentials, curUserPattern); + User user = getUser(connection, username, credentials, curUserPattern); if (user != null) { try { // Check the user's credentials - if (checkCredentials(context, user, credentials)) { + if (checkCredentials(connection.context, user, credentials)) { // Search for additional roles - List<String> roles = getRoles(context, user); + List<String> roles = getRoles(connection, user); if (containerLog.isDebugEnabled()) { containerLog.debug("Found roles: " + roles.toString()); } @@ -1411,16 +1336,18 @@ public class JNDIRealm extends RealmBase return null; } else { // Retrieve user information - User user = getUser(context, username, credentials); - if (user == null) + User user = getUser(connection, username, credentials); + if (user == null) { return null; + } // Check the user's credentials - if (!checkCredentials(context, user, credentials)) + if (!checkCredentials(connection.context, user, credentials)) { return null; + } // Search for additional roles - List<String> roles = getRoles(context, user); + List<String> roles = getRoles(connection, user); if (containerLog.isDebugEnabled()) { containerLog.debug("Found roles: " + roles.toString()); } @@ -1431,22 +1358,22 @@ public class JNDIRealm extends RealmBase } + // ------------------------------------------------------ Protected Methods + /** * Return a User object containing information about the user * with the specified username, if found in the directory; * otherwise return <code>null</code>. * - * @param context The directory context + * @param connection The directory context * @param username Username to be looked up * @return the User object * @exception NamingException if a directory server error occurs * - * @see #getUser(DirContext, String, String, int) + * @see #getUser(JNDIConnection, String, String, int) */ - protected User getUser(DirContext context, String username) - throws NamingException { - - return getUser(context, username, null, -1); + protected User getUser(JNDIConnection connection, String username) throws NamingException { + return getUser(connection, username, null, -1); } @@ -1455,18 +1382,16 @@ public class JNDIRealm extends RealmBase * with the specified username, if found in the directory; * otherwise return <code>null</code>. * - * @param context The directory context + * @param connection The directory context * @param username Username to be looked up * @param credentials User credentials (optional) * @return the User object * @exception NamingException if a directory server error occurs * - * @see #getUser(DirContext, String, String, int) + * @see #getUser(JNDIConnection, String, String, int) */ - protected User getUser(DirContext context, String username, String credentials) - throws NamingException { - - return getUser(context, username, credentials, -1); + protected User getUser(JNDIConnection connection, String username, String credentials) throws NamingException { + return getUser(connection, username, credentials, -1); } @@ -1481,25 +1406,26 @@ public class JNDIRealm extends RealmBase * configuration attribute is specified, all values of that * attribute are retrieved from the directory entry. * - * @param context The directory context + * @param connection The directory context * @param username Username to be looked up * @param credentials User credentials (optional) * @param curUserPattern Index into userPatternFormatArray * @return the User object * @exception NamingException if a directory server error occurs */ - protected User getUser(DirContext context, String username, - String credentials, int curUserPattern) - throws NamingException { + protected User getUser(JNDIConnection connection, String username, String credentials, int curUserPattern) + throws NamingException { User user = null; // Get attributes to retrieve from user entry List<String> list = new ArrayList<>(); - if (userPassword != null) + if (userPassword != null) { list.add(userPassword); - if (userRoleName != null) + } + if (userRoleName != null) { list.add(userRoleName); + } if (userRoleAttribute != null) { list.add(userRoleAttribute); } @@ -1507,8 +1433,8 @@ public class JNDIRealm extends RealmBase list.toArray(attrIds); // Use pattern or search for user entry - if (userPatternFormatArray != null && curUserPattern >= 0) { - user = getUserByPattern(context, username, credentials, attrIds, curUserPattern); + if (userPatternArray != null && curUserPattern >= 0) { + user = getUserByPattern(connection, username, credentials, attrIds, curUserPattern); if (containerLog.isDebugEnabled()) { containerLog.debug("Found user by pattern [" + user + "]"); } @@ -1516,12 +1442,12 @@ public class JNDIRealm extends RealmBase boolean thisUserSearchAsUser = isUserSearchAsUser(); try { if (thisUserSearchAsUser) { - userCredentialsAdd(context, username, credentials); + userCredentialsAdd(connection.context, username, credentials); } - user = getUserBySearch(context, username, attrIds); + user = getUserBySearch(connection, username, attrIds); } finally { if (thisUserSearchAsUser) { - userCredentialsRemove(context); + userCredentialsRemove(connection.context); } } if (containerLog.isDebugEnabled()) { @@ -1531,8 +1457,7 @@ public class JNDIRealm extends RealmBase if (userPassword == null && credentials != null && user != null) { // The password is available. Insert it since it may be required for // role searches. - return new User(user.getUserName(), user.getDN(), credentials, - user.getRoles(), user.getUserRoleId()); + return new User(user.getUserName(), user.getDN(), credentials, user.getRoles(), user.getUserRoleId()); } return user; @@ -1552,11 +1477,8 @@ public class JNDIRealm extends RealmBase * @return the User object * @exception NamingException if a directory server error occurs */ - protected User getUserByPattern(DirContext context, - String username, - String[] attrIds, - String dn) - throws NamingException { + protected User getUserByPattern(DirContext context, String username, String[] attrIds, String dn) + throws NamingException { // If no attributes are requested, no need to look for them if (attrIds == null || attrIds.length == 0) { @@ -1570,13 +1492,15 @@ public class JNDIRealm extends RealmBase } catch (NameNotFoundException e) { return null; } - if (attrs == null) + if (attrs == null) { return null; + } // Retrieve value of userPassword String password = null; - if (userPassword != null) + if (userPassword != null) { password = getAttributeValue(userPassword, attrs); + } String userRoleAttrValue = null; if (userRoleAttribute != null) { @@ -1585,8 +1509,9 @@ public class JNDIRealm extends RealmBase // Retrieve values of userRoleName attribute ArrayList<String> roles = null; - if (userRoleName != null) + if (userRoleName != null) { roles = addAttributeValues(userRoleName, attrs, roles); + } return new User(username, dn, password, roles, userRoleAttrValue); } @@ -1598,7 +1523,7 @@ public class JNDIRealm extends RealmBase * username and return a User object; otherwise return * <code>null</code>. * - * @param context The directory context + * @param connection The directory context * @param username The username * @param credentials User credentials (optional) * @param attrIds String[]containing names of attributes to @@ -1607,34 +1532,34 @@ public class JNDIRealm extends RealmBase * @exception NamingException if a directory server error occurs * @see #getUserByPattern(DirContext, String, String[], String) */ - protected User getUserByPattern(DirContext context, - String username, - String credentials, - String[] attrIds, - int curUserPattern) - throws NamingException { + protected User getUserByPattern(JNDIConnection connection, String username, String credentials, String[] attrIds, + int curUserPattern) throws NamingException { User user = null; - if (username == null || userPatternFormatArray[curUserPattern] == null) + if (username == null || userPatternArray[curUserPattern] == null) { return null; + } - // Form the dn from the user pattern - String dn = userPatternFormatArray[curUserPattern].format(new String[] { username }); + // Form the DistinguishedName from the user pattern. + // Escape in case username contains a character with special meaning in + // an attribute value. + String dn = connection.userPatternFormatArray[curUserPattern].format( + new String[] { doAttributeValueEscaping(username) }); try { - user = getUserByPattern(context, username, attrIds, dn); + user = getUserByPattern(connection.context, username, attrIds, dn); } catch (NameNotFoundException e) { return null; } catch (NamingException e) { // If the getUserByPattern() call fails, try it again with the // credentials of the user that we're searching for try { - userCredentialsAdd(context, dn, credentials); + userCredentialsAdd(connection.context, dn, credentials); - user = getUserByPattern(context, username, attrIds, dn); + user = getUserByPattern(connection.context, username, attrIds, dn); } finally { - userCredentialsRemove(context); + userCredentialsRemove(connection.context); } } return user; @@ -1646,22 +1571,23 @@ public class JNDIRealm extends RealmBase * information about the user with the specified username, if * found in the directory; otherwise return <code>null</code>. * - * @param context The directory context + * @param connection The directory context * @param username The username * @param attrIds String[]containing names of attributes to retrieve. * @return the User object * @exception NamingException if a directory server error occurs */ - protected User getUserBySearch(DirContext context, - String username, - String[] attrIds) - throws NamingException { + protected User getUserBySearch(JNDIConnection connection, String username, String[] attrIds) + throws NamingException { - if (username == null || userSearchFormat == null) + if (username == null || connection.userSearchFormat == null) { return null; + } // Form the search filter - String filter = userSearchFormat.format(new String[] { username }); + // Escape in case username contains a character with special meaning in + // a search filter. + String filter = connection.userSearchFormat.format(new String[] { doFilterEscaping(username) }); // Set up the search controls SearchControls constraints = new SearchControls(); @@ -1676,12 +1602,12 @@ public class JNDIRealm extends RealmBase constraints.setTimeLimit(timeLimit); // Specify the attributes to be retrieved - if (attrIds == null) + if (attrIds == null) { attrIds = new String[0]; + } constraints.setReturningAttributes(attrIds); - NamingEnumeration<SearchResult> results = - context.search(userBase, filter, constraints); + NamingEnumeration<SearchResult> results = connection.context.search(userBase, filter, constraints); try { // Fail if no entries found @@ -1690,10 +1616,11 @@ public class JNDIRealm extends RealmBase return null; } } catch (PartialResultException ex) { - if (!adCompat) + if (!adCompat) { throw ex; - else + } else { return null; + } } // Get result for the first entry found @@ -1702,29 +1629,34 @@ public class JNDIRealm extends RealmBase // Check no further entries were found try { if (results.hasMore()) { - if(containerLog.isInfoEnabled()) - containerLog.info("username " + username + " has multiple entries"); + if (containerLog.isInfoEnabled()) { + containerLog.info(sm.getString("jndiRealm.multipleEntries", username)); + } return null; } } catch (PartialResultException ex) { - if (!adCompat) + if (!adCompat) { throw ex; + } } - String dn = getDistinguishedName(context, userBase, result); + String dn = getDistinguishedName(connection.context, userBase, result); - if (containerLog.isTraceEnabled()) + if (containerLog.isTraceEnabled()) { containerLog.trace(" entry found for " + username + " with dn " + dn); + } // Get the entry's attributes Attributes attrs = result.getAttributes(); - if (attrs == null) + if (attrs == null) { return null; + } // Retrieve value of userPassword String password = null; - if (userPassword != null) + if (userPassword != null) { password = getAttributeValue(userPassword, attrs); + } String userRoleAttrValue = null; if (userRoleAttribute != null) { @@ -1733,8 +1665,9 @@ public class JNDIRealm extends RealmBase // Retrieve values of userRoleName attribute ArrayList<String> roles = null; - if (userRoleName != null) + if (userRoleName != null) { roles = addAttributeValues(userRoleName, attrs, roles); + } return new User(username, dn, password, roles, userRoleAttrValue); } finally { @@ -1760,30 +1693,25 @@ public class JNDIRealm extends RealmBase * @return <code>true</code> if the credentials are validated * @exception NamingException if a directory server error occurs */ - protected boolean checkCredentials(DirContext context, - User user, - String credentials) - throws NamingException { - - boolean validated = false; - - if (userPassword == null) { - validated = bindAsUser(context, user, credentials); - } else { - validated = compareCredentials(context, user, credentials); - } - - if (containerLog.isTraceEnabled()) { - if (validated) { - containerLog.trace(sm.getString("jndiRealm.authenticateSuccess", - user.getUserName())); - } else { - containerLog.trace(sm.getString("jndiRealm.authenticateFailure", - user.getUserName())); - } - } - return validated; - } + protected boolean checkCredentials(DirContext context, User user, String credentials) throws NamingException { + + boolean validated = false; + + if (userPassword == null) { + validated = bindAsUser(context, user, credentials); + } else { + validated = compareCredentials(context, user, credentials); + } + + if (containerLog.isTraceEnabled()) { + if (validated) { + containerLog.trace(sm.getString("jndiRealm.authenticateSuccess", user.getUserName())); + } else { + containerLog.trace(sm.getString("jndiRealm.authenticateFailure", user.getUserName())); + } + } + return validated; + } /** @@ -1796,17 +1724,15 @@ public class JNDIRealm extends RealmBase * @return <code>true</code> if the credentials are validated * @exception NamingException if a directory server error occurs */ - protected boolean compareCredentials(DirContext context, - User info, - String credentials) - throws NamingException { - + protected boolean compareCredentials(DirContext context, User info, String credentials) throws NamingException { // Validate the credentials specified by the user - if (containerLog.isTraceEnabled()) + if (containerLog.isTraceEnabled()) { containerLog.trace(" validating credentials"); + } - if (info == null || credentials == null) + if (info == null || credentials == null) { return false; + } String password = info.getPassword(); @@ -1823,21 +1749,22 @@ public class JNDIRealm extends RealmBase * @return <code>true</code> if the credentials are validated * @exception NamingException if a directory server error occurs */ - protected boolean bindAsUser(DirContext context, - User user, - String credentials) - throws NamingException { - - if (credentials == null || user == null) - return false; - - String dn = user.getDN(); - if (dn == null) - return false; - - // Validate the credentials specified by the user - if (containerLog.isTraceEnabled()) { - containerLog.trace(" validating credentials by binding as the user"); + protected boolean bindAsUser(DirContext context, User user, String credentials) throws NamingException { + + if (credentials == null || user == null) { + return false; + } + + // This is returned from the directory so will be attribute value + // escaped if required + String dn = user.getDN(); + if (dn == null) { + return false; + } + + // Validate the credentials specified by the user + if (containerLog.isTraceEnabled()) { + containerLog.trace(" validating credentials by binding as the user"); } userCredentialsAdd(context, dn, credentials); @@ -1862,74 +1789,79 @@ public class JNDIRealm extends RealmBase return validated; } - /** - * Configure the context to use the provided credentials for - * authentication. - * - * @param context DirContext to configure - * @param dn Distinguished name of user - * @param credentials Credentials of user - * @exception NamingException if a directory server error occurs - */ - private void userCredentialsAdd(DirContext context, String dn, - String credentials) throws NamingException { + + /** + * Configure the context to use the provided credentials for + * authentication. + * + * @param context DirContext to configure + * @param dn Distinguished name of user + * @param credentials Credentials of user + * @exception NamingException if a directory server error occurs + */ + private void userCredentialsAdd(DirContext context, String dn, String credentials) throws NamingException { // Set up security environment to bind as the user context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn); context.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials); } + /** * Configure the context to use {@link #connectionName} and * {@link #connectionPassword} if specified or an anonymous connection if * those attributes are not specified. * - * @param context DirContext to configure - * @exception NamingException if a directory server error occurs + * @param context DirContext to configure + * @exception NamingException if a directory server error occurs */ - private void userCredentialsRemove(DirContext context) - throws NamingException { + private void userCredentialsRemove(DirContext context) throws NamingException { // Restore the original security environment if (connectionName != null) { - context.addToEnvironment(Context.SECURITY_PRINCIPAL, - connectionName); + context.addToEnvironment(Context.SECURITY_PRINCIPAL, connectionName); } else { context.removeFromEnvironment(Context.SECURITY_PRINCIPAL); } if (connectionPassword != null) { - context.addToEnvironment(Context.SECURITY_CREDENTIALS, - connectionPassword); + context.addToEnvironment(Context.SECURITY_CREDENTIALS, connectionPassword); } else { context.removeFromEnvironment(Context.SECURITY_CREDENTIALS); } } + /** * Return a List of roles associated with the given User. Any * roles present in the user's directory entry are supplemented by * a directory search. If no roles are associated with this user, * a zero-length List is returned. * - * @param context The directory context we are searching + * @param connection The directory context we are searching * @param user The User to be checked * @return the list of role names * @exception NamingException if a directory server error occurs */ - protected List<String> getRoles(DirContext context, User user) - throws NamingException { + protected List<String> getRoles(JNDIConnection connection, User user) throws NamingException { - if (user == null) + if (user == null) { return null; + } + // This is returned from the directory so will be attribute value + // escaped if required String dn = user.getDN(); + // This is the name the user provided to the authentication process so + // it will not be escaped String username = user.getUserName(); String userRoleId = user.getUserRoleId(); - if (dn == null || username == null) + if (dn == null || username == null) { return null; + } - if (containerLog.isTraceEnabled()) + if (containerLog.isTraceEnabled()) { containerLog.trace(" getRoles(" + dn + ")"); + } // Start with roles retrieved from the user entry List<String> list = new ArrayList<>(); @@ -1937,8 +1869,9 @@ public class JNDIRealm extends RealmBase if (userRoles != null) { list.addAll(userRoles); } - if (commonRole != null) + if (commonRole != null) { list.add(commonRole); + } if (containerLog.isTraceEnabled()) { containerLog.trace(" Found " + list.size() + " user internal roles"); @@ -1946,54 +1879,66 @@ public class JNDIRealm extends RealmBase } // Are we configured to do role searches? - if ((roleFormat == null) || (roleName == null)) + if ((connection.roleFormat == null) || (roleName == null)) { return list; + } - // Set up parameters for an appropriate search - String filter = roleFormat.format(new String[] { doRFC2254Encoding(dn), username, userRoleId }); + // Set up parameters for an appropriate search filter + // The dn is already attribute value escaped but the others are not + // This is a filter so all input will require filter escaping + String filter = connection.roleFormat.format(new String[] { + doFilterEscaping(dn), + doFilterEscaping(doAttributeValueEscaping(username)), + doFilterEscaping(doAttributeValueEscaping(userRoleId)) }); SearchControls controls = new SearchControls(); - if (roleSubtree) + if (roleSubtree) { controls.setSearchScope(SearchControls.SUBTREE_SCOPE); - else + } else { controls.setSearchScope(SearchControls.ONELEVEL_SCOPE); + } controls.setReturningAttributes(new String[] {roleName}); String base = null; - if (roleBaseFormat != null) { - NameParser np = context.getNameParser(""); + if (connection.roleBaseFormat != null) { + NameParser np = connection.context.getNameParser(""); Name name = np.parse(dn); String nameParts[] = new String[name.size()]; for (int i = 0; i < name.size(); i++) { - nameParts[i] = name.get(i); + // May have been returned with \<char> escaping rather than + // \<hex><hex>. Make sure it is \<hex><hex>. + nameParts[i] = convertToHexEscape(name.get(i)); } - base = roleBaseFormat.format(nameParts); + base = connection.roleBaseFormat.format(nameParts); } else { base = ""; } // Perform the configured search and process the results - NamingEnumeration<SearchResult> results = searchAsUser(context, user, base, filter, controls, + NamingEnumeration<SearchResult> results = searchAsUser(connection.context, user, base, filter, controls, isRoleSearchAsUser()); - if (results == null) + if (results == null) { return list; // Should never happen, but just in case ... + } Map<String, String> groupMap = new HashMap<>(); try { while (results.hasMore()) { SearchResult result = results.next(); Attributes attrs = result.getAttributes(); - if (attrs == null) + if (attrs == null) { continue; - String dname = getDistinguishedName(context, roleBase, result); + } + String dname = getDistinguishedName(connection.context, base, result); String name = getAttributeValue(roleName, attrs); if (name != null && dname != null) { groupMap.put(dname, name); } } } catch (PartialResultException ex) { - if (!adCompat) + if (!adCompat) { throw ex; + } } finally { results.close(); } @@ -2018,23 +1963,29 @@ public class JNDIRealm extends RealmBase Map<String, String> newThisRound = new HashMap<>(); // Stores the groups we find in this iteration for (Entry<String, String> group : newGroups.entrySet()) { - filter = roleFormat.format(new String[] { doRFC2254Encoding(group.getKey()), - group.getValue(), group.getValue() }); + // Group key is already value escaped if required + // Group value is not value escaped + // Everything needs to be filter escaped + filter = connection.roleFormat.format(new String[] { + doFilterEscaping(group.getKey()), + doFilterEscaping(doAttributeValueEscaping(group.getValue())), + doFilterEscaping(doAttributeValueEscaping(group.getValue())) }); if (containerLog.isTraceEnabled()) { - containerLog.trace("Perform a nested group search with base "+ roleBase + " and filter " + filter); + containerLog.trace("Perform a nested group search with base "+ roleBase + + " and filter " + filter); } - results = searchAsUser(context, user, roleBase, filter, controls, - isRoleSearchAsUser()); + results = searchAsUser(connection.context, user, base, filter, controls, isRoleSearchAsUser()); try { while (results.hasMore()) { SearchResult result = results.next(); Attributes attrs = result.getAttributes(); - if (attrs == null) + if (attrs == null) { continue; - String dname = getDistinguishedName(context, roleBase, result); + } + String dname = getDistinguishedName(connection.context, roleBase, result); String name = getAttributeValue(roleName, attrs); if (name != null && dname != null && !groupMap.keySet().contains(dname)) { groupMap.put(dname, name); @@ -2043,12 +1994,12 @@ public class JNDIRealm extends RealmBase if (containerLog.isTraceEnabled()) { containerLog.trace(" Found nested role " + dname + " -> " + name); } - } - } + } } catch (PartialResultException ex) { - if (!adCompat) + if (!adCompat) { throw ex; + } } finally { results.close(); } @@ -2062,6 +2013,7 @@ public class JNDIRealm extends RealmBase return list; } + /** * Perform the search on the context as the {@code dn}, when * {@code searchAsUser} is {@code true}, otherwise search the context with @@ -2084,8 +2036,7 @@ public class JNDIRealm extends RealmBase * @throws NamingException * if a directory server error occurs */ - private NamingEnumeration<SearchResult> searchAsUser(DirContext context, - User user, String base, String filter, + private NamingEnumeration<SearchResult> searchAsUser(DirContext context, User user, String base, String filter, SearchControls controls, boolean searchAsUser) throws NamingException { NamingEnumeration<SearchResult> results; try { @@ -2110,26 +2061,30 @@ public class JNDIRealm extends RealmBase * @return the attribute value * @exception NamingException if a directory server error occurs */ - private String getAttributeValue(String attrId, Attributes attrs) - throws NamingException { + private String getAttributeValue(String attrId, Attributes attrs) throws NamingException { - if (containerLog.isTraceEnabled()) + if (containerLog.isTraceEnabled()) { containerLog.trace(" retrieving attribute " + attrId); + } - if (attrId == null || attrs == null) + if (attrId == null || attrs == null) { return null; + } Attribute attr = attrs.get(attrId); - if (attr == null) + if (attr == null) { return null; + } Object value = attr.get(); - if (value == null) + if (value == null) { return null; + } String valueString = null; - if (value instanceof byte[]) + if (value instanceof byte[]) { valueString = new String((byte[]) value); - else + } else { valueString = value.toString(); + } return valueString; } @@ -2144,20 +2099,22 @@ public class JNDIRealm extends RealmBase * @return the list of attribute values * @exception NamingException if a directory server error occurs */ - private ArrayList<String> addAttributeValues(String attrId, - Attributes attrs, - ArrayList<String> values) - throws NamingException{ + private ArrayList<String> addAttributeValues(String attrId, Attributes attrs, ArrayList<String> values) + throws NamingException { - if (containerLog.isTraceEnabled()) + if (containerLog.isTraceEnabled()) { containerLog.trace(" retrieving values for attribute " + attrId); - if (attrId == null || attrs == null) + } + if (attrId == null || attrs == null) { return values; - if (values == null) + } + if (values == null) { values = new ArrayList<>(); + } Attribute attr = attrs.get(attrId); - if (attr == null) + if (attr == null) { return values; + } NamingEnumeration<?> e = attr.getAll(); try { while(e.hasMore()) { @@ -2165,8 +2122,9 @@ public class JNDIRealm extends RealmBase values.add(value); } } catch (PartialResultException ex) { - if (!adCompat) + if (!adCompat) { throw ex; + } } finally { e.close(); } @@ -2177,13 +2135,17 @@ public class JNDIRealm extends RealmBase /** * Close any open connection to the directory server for this Realm. * - * @param context The directory context to be closed + * @param connection The directory context to be closed */ - protected void close(DirContext context) { + protected void close(JNDIConnection connection) { // Do nothing if there is no opened connection - if (context == null) + if (connection == null || connection.context == null) { + if (connectionPool == null) { + singleConnectionLock.unlock(); + } return; + } // Close tls startResponse if used if (tls != null) { @@ -2195,14 +2157,34 @@ public class JNDIRealm extends RealmBase } // Close our opened connection try { - if (containerLog.isDebugEnabled()) + if (containerLog.isDebugEnabled()) { containerLog.debug("Closing directory context"); - context.close(); + } + connection.context.close(); } catch (NamingException e) { containerLog.error(sm.getString("jndiRealm.close"), e); } - this.context = null; + connection.context = null; + // The lock will be reacquired before any manipulation of the connection + if (connectionPool == null) { + singleConnectionLock.unlock(); + } + } + + /** + * Close all pooled connections. + */ + protected void closePooledConnections() { + if (connectionPool != null) { + // Close any pooled connections as they might be bad as well + synchronized (connectionPool) { + JNDIConnection connection = null; + while ((connection = connectionPool.pop()) != null) { + close(connection); + } + } + } } @@ -2218,8 +2200,34 @@ public class JNDIRealm extends RealmBase return null; } + JNDIConnection connection = null; + User user = null; try { - User user = getUser(open(), username, null); + // Ensure that we have a directory context available + connection = get(); + + // Occasionally the directory context will timeout. Try one more + // time before giving up. + try { + user = getUser(connection, username, null); + } catch (NullPointerException | NamingException e) { + // log the exception so we know it's there. + containerLog.info(sm.getString("jndiRealm.exception.retry"), e); + + // close the connection so we know it will be reopened. + close(connection); + closePooledConnections(); + + // open a new directory context. + connection = get(); + + // Try the authentication again. + user = getUser(connection, username, null); + } + + // Release this context + release(connection); + if (user == null) { // User should be found... return null; @@ -2228,11 +2236,13 @@ public class JNDIRealm extends RealmBase return user.getPassword(); } } catch (NamingException e) { + // Log the problem for posterity + containerLog.error(sm.getString("jndiRealm.exception"), e); return null; } - } + /** * Get the principal associated with the specified certificate. * @param username The user name @@ -2243,9 +2253,9 @@ public class JNDIRealm extends RealmBase return getPrincipal(username, null); } + @Override - protected Principal getPrincipal(GSSName gssName, - GSSCredential gssCredential) { + protected Principal getPrincipal(GSSName gssName, GSSCredential gssCredential) { String name = gssName.toString(); if (isStripRealmForGss()) { @@ -2259,120 +2269,102 @@ public class JNDIRealm extends RealmBase return getPrincipal(name, gssCredential); } + @Override - protected Principal getPrincipal(String username, - GSSCredential gssCredential) { + protected Principal getPrincipal(String username, GSSCredential gssCredential) { - DirContext context = null; + JNDIConnection connection = null; Principal principal = null; try { - // Ensure that we have a directory context available - context = open(); + connection = get(); // Occasionally the directory context will timeout. Try one more // time before giving up. try { // Authenticate the specified username if possible - principal = getPrincipal(context, username, gssCredential); + principal = getPrincipal(connection, username, gssCredential); } catch (CommunicationException | ServiceUnavailableException e) { - // log the exception so we know it's there. containerLog.info(sm.getString("jndiRealm.exception.retry"), e); // close the connection so we know it will be reopened. - if (context != null) - close(context); + close(connection); + closePooledConnections(); // open a new directory context. - context = open(); + connection = get(); // Try the authentication again. - principal = getPrincipal(context, username, gssCredential); - + principal = getPrincipal(connection, username, gssCredential); } - // Release this context - release(context); + release(connection); // Return the authenticated Principal (if any) return principal; } catch (NamingException e) { - // Log the problem for posterity containerLog.error(sm.getString("jndiRealm.exception"), e); - // Close the connection so that it gets reopened next time - if (context != null) - close(context); - // Return "not authenticated" for this request return null; - } - - } /** * Get the principal associated with the specified certificate. - * @param context The directory context + * @param connection The directory context * @param username The user name * @param gssCredential The credentials * @return the Principal associated with the given certificate. * @exception NamingException if a directory server error occurs */ - protected synchronized Principal getPrincipal(DirContext context, - String username, GSSCredential gssCredential) - throws NamingException { + protected Principal getPrincipal(JNDIConnection connection, String username, GSSCredential gssCredential) + throws NamingException { User user = null; List<String> roles = null; Hashtable<?, ?> preservedEnvironment = null; + DirContext context = connection.context; try { if (gssCredential != null && isUseDelegatedCredential()) { // Preserve the current context environment parameters preservedEnvironment = context.getEnvironment(); // Set up context - context.addToEnvironment( - Context.SECURITY_AUTHENTICATION, "GSSAPI"); - context.addToEnvironment( - "javax.security.sasl.server.authentication", "true"); - context.addToEnvironment( - "javax.security.sasl.qop", spnegoDelegationQop); + context.addToEnvironment(Context.SECURITY_AUTHENTICATION, "GSSAPI"); + context.addToEnvironment("javax.security.sasl.server.authentication", "true"); + context.addToEnvironment("javax.security.sasl.qop", spnegoDelegationQop); // Note: Subject already set in SPNEGO authenticator so no need // for Subject.doAs() here } - user = getUser(context, username); + user = getUser(connection, username); if (user != null) { - roles = getRoles(context, user); + roles = getRoles(connection, user); } } finally { if (gssCredential != null && isUseDelegatedCredential()) { - restoreEnvironmentParameter(context, - Context.SECURITY_AUTHENTICATION, preservedEnvironment); - restoreEnvironmentParameter(context, - "javax.security.sasl.server.authentication", preservedEnvironment); - restoreEnvironmentParameter(context, "javax.security.sasl.qop", - preservedEnvironment); + restoreEnvironmentParameter(context, Context.SECURITY_AUTHENTICATION, preservedEnvironment); + restoreEnvironmentParameter(context, "javax.security.sasl.server.authentication", preservedEnvironment); + restoreEnvironmentParameter(context, "javax.security.sasl.qop", preservedEnvironment); } } if (user != null) { - return new GenericPrincipal(user.getUserName(), user.getPassword(), - roles, null, null, gssCredential); + return new GenericPrincipal(user.getUserName(), user.getPassword(), roles, null, null, gssCredential); } return null; } + private void restoreEnvironmentParameter(DirContext context, String parameterName, Hashtable<?, ?> preservedEnvironment) { try { @@ -2386,55 +2378,118 @@ public class JNDIRealm extends RealmBase } } + /** * Open (if necessary) and return a connection to the configured * directory server for this Realm. - * @return the directory context + * @return the connection * @exception NamingException if a directory server error occurs */ - protected DirContext open() throws NamingException { + protected JNDIConnection get() throws NamingException { + JNDIConnection connection = null; + // Use the pool if available, otherwise use the single connection + if (connectionPool != null) { + connection = connectionPool.pop(); + if (connection == null) { + connection = create(); + } + } else { + singleConnectionLock.lock(); + connection = singleConnection; + } + if (connection.context == null) { + open(connection); + } + return connection; + } - // Do nothing if there is a directory server connection already open - if (context != null) - return context; - try { + /** + * Release our use of this connection so that it can be recycled. + * + * @param connection The directory context to release + */ + protected void release(JNDIConnection connection) { + if (connectionPool != null) { + if (!connectionPool.push(connection)) { + // Any connection that doesn't end back to the pool must be closed + close(connection); + } + } else { + singleConnectionLock.unlock(); + } + } - // Ensure that we have a directory context available - context = createDirContext(getDirectoryContextEnvironment()); + /** + * Create a new connection wrapper, along with the + * message formats. + * @return the new connection + */ + protected JNDIConnection create() { + JNDIConnection connection = new JNDIConnection(); + if (userSearch != null) { + connection.userSearchFormat = new MessageFormat(userSearch); + } + if (userPattern != null) { + int len = userPatternArray.length; + connection.userPatternFormatArray = new MessageFormat[len]; + for (int i = 0; i < len; i++) { + connection.userPatternFormatArray[i] = new MessageFormat(userPatternArray[i]); + } + } + if (roleBase != null) { + connection.roleBaseFormat = new MessageFormat(roleBase); + } + if (roleSearch != null) { + connection.roleFormat = new MessageFormat(roleSearch); + } + return connection; + } + + + /** + * Create a new connection to the directory server. + * @param connection The directory server connection wrapper + * @throws NamingException if a directory server error occurs + */ + protected void open(JNDIConnection connection) throws NamingException { + ClassLoader ocl = null; + try { + if (!isUseContextClassLoader()) { + ocl = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader()); + } + // Ensure that we have a directory context available + connection.context = createDirContext(getDirectoryContextEnvironment()); } catch (Exception e) { if (alternateURL == null || alternateURL.length() == 0) { // No alternate URL. Re-throw the exception. throw e; } - connectionAttempt = 1; - // log the first exception. containerLog.info(sm.getString("jndiRealm.exception.retry"), e); - // Try connecting to the alternate url. - context = createDirContext(getDirectoryContextEnvironment()); - + connection.context = createDirContext(getDirectoryContextEnvironment()); } finally { - // reset it in case the connection times out. // the primary may come back. connectionAttempt = 0; - + if (!isUseContextClassLoader()) { + Thread.currentThread().setContextClassLoader(ocl); + } } - - return context; - } + @Override public boolean isAvailable() { // Simple best effort check - return (context != null); + return (connectionPool != null || singleConnection.context != null); } + private DirContext createDirContext(Hashtable<String, String> env) throws NamingException { if (useStartTls) { return createTlsDirContext(env); @@ -2443,13 +2498,13 @@ public class JNDIRealm extends RealmBase } } + private SSLSocketFactory getSSLSocketFactory() { if (sslSocketFactory != null) { return sslSocketFactory; } final SSLSocketFactory result; - if (this.sslSocketFactoryClassName != null - && !sslSocketFactoryClassName.trim().equals("")) { + if (this.sslSocketFactoryClassName != null && !sslSocketFactoryClassName.trim().equals("")) { result = createSSLSocketFactoryFromClassName(this.sslSocketFactoryClassName); } else { result = createSSLContextFactoryFromProtocol(sslProtocol); @@ -2458,6 +2513,7 @@ public class JNDIRealm extends RealmBase return result; } + private SSLSocketFactory createSSLSocketFactoryFromClassName(String className) { try { Object o = constructInstance(className); @@ -2475,6 +2531,7 @@ public class JNDIRealm extends RealmBase } } + private SSLSocketFactory createSSLContextFactoryFromProtocol(String protocol) { try { SSLContext sslContext; @@ -2486,14 +2543,13 @@ public class JNDIRealm extends RealmBase } return sslContext.getSocketFactory(); } catch (NoSuchAlgorithmException | KeyManagementException e) { - List<String> allowedProtocols = Arrays - .asList(getSupportedSslProtocols()); - throw new IllegalArgumentException( - sm.getString("jndiRealm.invalidSslProtocol", protocol, - allowedProtocols), e); + List<String> allowedProtocols = Arrays.asList(getSupportedSslProtocols()); + throw new IllegalArgumentException(sm.getString("jndiRealm.invalidSslProtocol", + protocol, allowedProtocols), e); } } + /** * Create a tls enabled LdapContext and set the StartTlsResponse tls * instance variable. @@ -2504,12 +2560,10 @@ public class JNDIRealm extends RealmBase * @throws NamingException * when something goes wrong while negotiating the connection */ - private DirContext createTlsDirContext( - Hashtable<String, String> env) throws NamingException { + private DirContext createTlsDirContext(Hashtable<String, String> env) throws NamingException { Map<String, Object> savedEnv = new HashMap<>(); - for (String key : Arrays.asList(Context.SECURITY_AUTHENTICATION, - Context.SECURITY_CREDENTIALS, Context.SECURITY_PRINCIPAL, - Context.SECURITY_PROTOCOL)) { + for (String key : Arrays.asList(Context.SECURITY_AUTHENTICATION, Context.SECURITY_CREDENTIALS, + Context.SECURITY_PRINCIPAL, Context.SECURITY_PROTOCOL)) { Object entry = env.remove(key); if (entry != null) { savedEnv.put(key, entry); @@ -2518,8 +2572,7 @@ public class JNDIRealm extends RealmBase LdapContext result = null; try { result = new InitialLdapContext(env, null); - tls = (StartTlsResponse) result - .extendedOperation(new StartTlsRequest()); + tls = (StartTlsResponse) result.extendedOperation(new StartTlsRequest()); if (getHostnameVerifier() != null) { tls.setHostnameVerifier(getHostnameVerifier()); } @@ -2528,22 +2581,21 @@ public class JNDIRealm extends RealmBase } try { SSLSession negotiate = tls.negotiate(getSSLSocketFactory()); - containerLog.debug(sm.getString("jndiRealm.negotiatedTls", - negotiate.getProtocol())); + containerLog.debug(sm.getString("jndiRealm.negotiatedTls", negotiate.getProtocol())); } catch (IOException e) { throw new NamingException(e.getMessage()); } } finally { if (result != null) { for (Map.Entry<String, Object> savedEntry : savedEnv.entrySet()) { - result.addToEnvironment(savedEntry.getKey(), - savedEntry.getValue()); + result.addToEnvironment(savedEntry.getKey(), savedEntry.getValue()); } } } return result; } + /** * Create our directory context configuration. * @@ -2554,52 +2606,48 @@ public class JNDIRealm extends RealmBase Hashtable<String,String> env = new Hashtable<>(); // Configure our directory context environment. - if (containerLog.isDebugEnabled() && connectionAttempt == 0) + if (containerLog.isDebugEnabled() && connectionAttempt == 0) { containerLog.debug("Connecting to URL " + connectionURL); - else if (containerLog.isDebugEnabled() && connectionAttempt > 0) + } else if (containerLog.isDebugEnabled() && connectionAttempt > 0) { containerLog.debug("Connecting to URL " + alternateURL); + } env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory); - if (connectionName != null) + if (connectionName != null) { env.put(Context.SECURITY_PRINCIPAL, connectionName); - if (connectionPassword != null) + } + if (connectionPassword != null) { env.put(Context.SECURITY_CREDENTIALS, connectionPassword); - if (connectionURL != null && connectionAttempt == 0) + } + if (connectionURL != null && connectionAttempt == 0) { env.put(Context.PROVIDER_URL, connectionURL); - else if (alternateURL != null && connectionAttempt > 0) + } else if (alternateURL != null && connectionAttempt > 0) { env.put(Context.PROVIDER_URL, alternateURL); - if (authentication != null) + } + if (authentication != null) { env.put(Context.SECURITY_AUTHENTICATION, authentication); - if (protocol != null) + } + if (protocol != null) { env.put(Context.SECURITY_PROTOCOL, protocol); - if (referrals != null) + } + if (referrals != null) { env.put(Context.REFERRAL, referrals); - if (derefAliases != null) + } + if (derefAliases != null) { env.put(JNDIRealm.DEREF_ALIASES, derefAliases); - if (connectionTimeout != null) + } + if (connectionTimeout != null) { env.put("com.sun.jndi.ldap.connect.timeout", connectionTimeout); - if (readTimeout != null) + } + if (readTimeout != null) { env.put("com.sun.jndi.ldap.read.timeout", readTimeout); + } return env; - - } - - - /** - * Release our use of this connection so that it can be recycled. - * - * @param context The directory context to release - */ - protected void release(DirContext context) { - - // NO-OP since we are not pooling anything - } // ------------------------------------------------------ Lifecycle Methods - /** * Prepare for the beginning of active use of the public methods of this * component and implement the requirements of @@ -2611,15 +2659,22 @@ public class JNDIRealm extends RealmBase @Override protected void startInternal() throws LifecycleException { + if (connectionPoolSize != 1) { + connectionPool = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, connectionPoolSize); + } + // Check to see if the connection to the directory can be opened + JNDIConnection connection = null; try { - open(); + connection = get(); } catch (NamingException e) { // A failure here is not fatal as the directory may be unavailable // now but available later. Unavailability of the directory is not // fatal once the Realm has started so there is no reason for it to // be fatal when the Realm starts. containerLog.error(sm.getString("jndiRealm.open"), e); + } finally { + release(connection); } super.startInternal(); @@ -2634,16 +2689,20 @@ public class JNDIRealm extends RealmBase * @exception LifecycleException if this component detects a fatal error * that needs to be reported */ - @Override + @Override protected void stopInternal() throws LifecycleException { - super.stopInternal(); - // Close any open directory server connection - close(this.context); - + if (connectionPool == null) { + singleConnectionLock.lock(); + close(singleConnection); + } else { + closePooledConnections(); + connectionPool = null; + } } + /** * Given a string containing LDAP patterns for user locations (separated by * parentheses in a pseudo-LDAP search string format - @@ -2678,8 +2737,7 @@ public class JNDIRealm extends RealmBase while (userPatternString.charAt(endParenLoc - 1) == '\\') { endParenLoc = userPatternString.indexOf(')', endParenLoc+1); } - String nextPathPart = userPatternString.substring - (startParenLoc+1, endParenLoc); + String nextPathPart = userPatternString.substring(startParenLoc+1, endParenLoc); pathList.add(nextPathPart); startingPoint = endParenLoc+1; startParenLoc = userPatternString.indexOf('(', startingPoint); @@ -2687,7 +2745,6 @@ public class JNDIRealm extends RealmBase return pathList.toArray(new String[] {}); } return null; - } @@ -2702,10 +2759,36 @@ public class JNDIRealm extends RealmBase * ) -> \29 * \ -> \5c * \0 -> \00 + * * @param inString string to escape according to RFC 2254 guidelines + * * @return String the escaped/encoded result + * + * @deprecated Will be removed in Tomcat 10.1.x onwards */ + @Deprecated protected String doRFC2254Encoding(String inString) { + return doFilterEscaping(inString); + } + + + /** + * Given an LDAP search string, returns the string with certain characters + * escaped according to RFC 2254 guidelines. + * The character mapping is as follows: + * char -> Replacement + * --------------------------- + * * -> \2a + * ( -> \28 + * ) -> \29 + * \ -> \5c + * \0 -> \00 + * + * @param inString string to escape according to RFC 2254 guidelines + * + * @return String the escaped/encoded result + */ + protected String doFilterEscaping(String inString) { StringBuilder buf = new StringBuilder(inString.length()); for (int i = 0; i < inString.length(); i++) { char c = inString.charAt(i); @@ -2743,47 +2826,42 @@ public class JNDIRealm extends RealmBase * @return String containing the distinguished name * @exception NamingException if a directory server error occurs */ - protected String getDistinguishedName(DirContext context, String base, - SearchResult result) throws NamingException { + protected String getDistinguishedName(DirContext context, String base, SearchResult result) throws NamingException { // Get the entry's distinguished name. For relative results, this means // we need to composite a name with the base name, the context name, and // the result name. For non-relative names, use the returned name. String resultName = result.getName(); Name name; if (result.isRelative()) { - if (containerLog.isTraceEnabled()) { - containerLog.trace(" search returned relative name: " + resultName); - } - NameParser parser = context.getNameParser(""); - Name contextName = parser.parse(context.getNameInNamespace()); - Name baseName = parser.parse(base); + if (containerLog.isTraceEnabled()) { + containerLog.trace(" search returned relative name: " + resultName); + } + NameParser parser = context.getNameParser(""); + Name contextName = parser.parse(context.getNameInNamespace()); + Name baseName = parser.parse(base); - // Bugzilla 32269 - Name entryName = parser.parse(new CompositeName(resultName).get(0)); + // Bugzilla 32269 + Name entryName = parser.parse(new CompositeName(resultName).get(0)); - name = contextName.addAll(baseName); - name = name.addAll(entryName); + name = contextName.addAll(baseName); + name = name.addAll(entryName); } else { - if (containerLog.isTraceEnabled()) { - containerLog.trace(" search returned absolute name: " + resultName); - } - try { - // Normalize the name by running it through the name parser. - NameParser parser = context.getNameParser(""); - URI userNameUri = new URI(resultName); - String pathComponent = userNameUri.getPath(); - // Should not ever have an empty path component, since that is /{DN} - if (pathComponent.length() < 1 ) { - throw new InvalidNameException( - "Search returned unparseable absolute name: " + - resultName ); - } - name = parser.parse(pathComponent.substring(1)); - } catch ( URISyntaxException e ) { - throw new InvalidNameException( - "Search returned unparseable absolute name: " + - resultName ); - } + if (containerLog.isTraceEnabled()) { + containerLog.trace(" search returned absolute name: " + resultName); + } + try { + // Normalize the name by running it through the name parser. + NameParser parser = context.getNameParser(""); + URI userNameUri = new URI(resultName); + String pathComponent = userNameUri.getPath(); + // Should not ever have an empty path component, since that is /{DN} + if (pathComponent.length() < 1 ) { + throw new InvalidNameException("Search returned unparseable absolute name: " + resultName); + } + name = parser.parse(pathComponent.substring(1)); + } catch ( URISyntaxException e ) { + throw new InvalidNameException("Search returned unparseable absolute name: " + resultName); + } } if (getForceDnHexEscape()) { @@ -2795,6 +2873,78 @@ public class JNDIRealm extends RealmBase } + /** + * Implements the necessary escaping to represent an attribute value as a + * String as per RFC 4514. + * + * @param input The original attribute value + * @return The string representation of the attribute value + */ + protected String doAttributeValueEscaping(String input) { + int len = input.length(); + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < len; i++) { + char c = input.charAt(i); + switch (c) { + case ' ': { + if (i == 0 || i == (len -1)) { + result.append("\\20"); + } else { + result.append(c); + } + break; + } + case '#': { + if (i == 0 ) { + result.append("\\23"); + } else { + result.append(c); + } + break; + } + case '\"': { + result.append("\\22"); + break; + } + case '+': { + result.append("\\2B"); + break; + } + case ',': { + result.append("\\2C"); + break; + } + case ';': { + result.append("\\3B"); + break; + } + case '<': { + result.append("\\3C"); + break; + } + case '>': { + result.append("\\3E"); + break; + } + case '\\': { + result.append("\\5C"); + break; + } + case '\u0000': { + result.append("\\00"); + break; + } + default: + result.append(c); + } + + } + + return result.toString(); + } + + protected static String convertToHexEscape(String input) { if (input.indexOf('\\') == -1) { // No escaping present. Return original. @@ -2871,7 +3021,7 @@ public class JNDIRealm extends RealmBase } - // ------------------------------------------------------ Private Classes + // ------------------------------------------------------ Protected Classes /** * A protected class representing a User @@ -2884,9 +3034,7 @@ public class JNDIRealm extends RealmBase private final List<String> roles; private final String userRoleId; - - public User(String username, String dn, String password, - List<String> roles, String userRoleId) { + public User(String username, String dn, String password, List<String> roles, String userRoleId) { this.username = username; this.dn = dn; this.password = password; @@ -2918,5 +3066,41 @@ public class JNDIRealm extends RealmBase return userRoleId; } } -} + + /** + * Class holding the connection to the directory plus the associated + * non thread safe message formats. + */ + protected static class JNDIConnection { + + /** + * The MessageFormat object associated with the current + * <code>userSearch</code>. + */ + protected MessageFormat userSearchFormat = null; + + /** + * An array of MessageFormat objects associated with the current + * <code>userPatternArray</code>. + */ + protected MessageFormat[] userPatternFormatArray = null; + + /** + * The MessageFormat object associated with the current + * <code>roleBase</code>. + */ + protected MessageFormat roleBaseFormat = null; + + /** + * The MessageFormat object associated with the current + * <code>roleSearch</code>. + */ + protected MessageFormat roleFormat = null; + + /** + * The directory context linking us to our directory server. + */ + protected DirContext context = null; + } +} Index: apache-tomcat-9.0.36-src/test/org/apache/catalina/realm/TestJNDIRealmIntegration.java =================================================================== --- /dev/null +++ apache-tomcat-9.0.36-src/test/org/apache/catalina/realm/TestJNDIRealmIntegration.java @@ -0,0 +1,263 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina.realm; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.juli.logging.LogFactory; + +import com.unboundid.ldap.listener.InMemoryDirectoryServer; +import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; +import com.unboundid.ldap.sdk.AddRequest; +import com.unboundid.ldap.sdk.LDAPConnection; +import com.unboundid.ldap.sdk.LDAPResult; +import com.unboundid.ldap.sdk.ResultCode; + +@RunWith(Parameterized.class) +public class TestJNDIRealmIntegration { + + private static final String USER_PATTERN = "cn={0},ou=people,dc=example,dc=com"; + private static final String USER_SEARCH = "cn={0}"; + private static final String USER_BASE = "ou=people,dc=example,dc=com"; + private static final String ROLE_SEARCH_A = "member={0}"; + private static final String ROLE_SEARCH_B = "member=cn={1},ou=people,dc=example,dc=com"; + private static final String ROLE_SEARCH_C = "member=cn={2},ou=people,dc=example,dc=com"; + private static final String ROLE_BASE = "ou=people,dc=example,dc=com"; + + private static InMemoryDirectoryServer ldapServer; + + @Parameterized.Parameters(name = "{index}: user[{5}], pwd[{6}]") + public static Collection<Object[]> parameters() { + List<Object[]> parameterSets = new ArrayList<>(); + for (String roleSearch : new String[] { ROLE_SEARCH_A, ROLE_SEARCH_B, ROLE_SEARCH_C }) { + addUsers(USER_PATTERN, null, null, roleSearch, ROLE_BASE, parameterSets); + addUsers(null, USER_SEARCH, USER_BASE, roleSearch, ROLE_BASE, parameterSets); + } + parameterSets.add(new Object[] { "cn={0},ou=s\\;ub,ou=people,dc=example,dc=com", null, null, ROLE_SEARCH_A, + "{3},ou=people,dc=example,dc=com", "testsub", "test", new String[] {"TestGroup4"} }); + return parameterSets; + } + + + private static void addUsers(String userPattern, String userSearch, String userBase, String roleSearch, + String roleBase, List<Object[]> parameterSets) { + parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase, + "test", "test", new String[] {"TestGroup"} }); + parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase, + "t;", "test", new String[] {"TestGroup"} }); + parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase, + "t*", "test", new String[] {"TestGroup"} }); + parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase, + "t=", "test", new String[] {"Test<Group*2", "Test>Group*3"} }); + } + + + @Parameter(0) + public String realmConfigUserPattern; + @Parameter(1) + public String realmConfigUserSearch; + @Parameter(2) + public String realmConfigUserBase; + @Parameter(3) + public String realmConfigRoleSearch; + @Parameter(4) + public String realmConfigRoleBase; + @Parameter(5) + public String username; + @Parameter(6) + public String credentials; + @Parameter(7) + public String[] groups; + + @Test + public void testAuthenication() throws Exception { + JNDIRealm realm = new JNDIRealm(); + realm.containerLog = LogFactory.getLog(TestJNDIRealmIntegration.class); + + realm.setConnectionURL("ldap://localhost:" + ldapServer.getListenPort()); + realm.setUserPattern(realmConfigUserPattern); + realm.setUserSearch(realmConfigUserSearch); + realm.setUserBase(realmConfigUserBase); + realm.setUserRoleAttribute("cn"); + realm.setRoleName("cn"); + realm.setRoleBase(realmConfigRoleBase); + realm.setRoleSearch(realmConfigRoleSearch); + realm.setRoleNested(true); + + GenericPrincipal p = (GenericPrincipal) realm.authenticate(username, credentials); + + Assert.assertNotNull(p); + Assert.assertEquals(username, p.name); + + Set<String> actualGroups = new HashSet<>(Arrays.asList(p.getRoles())); + Set<String> expectedGroups = new HashSet<>(Arrays.asList(groups)); + + Assert.assertEquals(expectedGroups.size(), actualGroups.size()); + Set<String> tmp = new HashSet<>(); + tmp.addAll(expectedGroups); + tmp.removeAll(actualGroups); + Assert.assertEquals(0, tmp.size()); + } + + + @BeforeClass + public static void createLDAP() throws Exception { + InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com"); + config.addAdditionalBindCredentials("cn=admin", "password"); + ldapServer = new InMemoryDirectoryServer(config); + + ldapServer.startListening(); + + try (LDAPConnection conn = ldapServer.getConnection()) { + + // Note: Only the DNs need attribute value escaping + AddRequest addBase = new AddRequest( + "dn: dc=example,dc=com", + "objectClass: top", + "objectClass: domain", + "dc: example"); + LDAPResult result = conn.processOperation(addBase); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addPeople = new AddRequest( + "dn: ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: organizationalUnit"); + result = conn.processOperation(addPeople); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addUserTest = new AddRequest( + "dn: cn=test,ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: person", + "objectClass: organizationalPerson", + "cn: test", + "sn: Test", + "userPassword: test"); + result = conn.processOperation(addUserTest); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addUserTestSemicolon = new AddRequest( + "dn: cn=t\\;,ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: person", + "objectClass: organizationalPerson", + "cn: t;", + "sn: Tsemicolon", + "userPassword: test"); + result = conn.processOperation(addUserTestSemicolon); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addUserTestAsterisk = new AddRequest( + "dn: cn=t*,ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: person", + "objectClass: organizationalPerson", + "cn: t*", + "sn: Tasterisk", + "userPassword: test"); + result = conn.processOperation(addUserTestAsterisk); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addUserTestEquals = new AddRequest( + "dn: cn=t\\=,ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: person", + "objectClass: organizationalPerson", + "cn: t=", + "sn: Tequals", + "userPassword: test"); + result = conn.processOperation(addUserTestEquals); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addGroupTest = new AddRequest( + "dn: cn=TestGroup,ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: groupOfNames", + "cn: TestGroup", + "member: cn=test,ou=people,dc=example,dc=com", + "member: cn=t\\;,ou=people,dc=example,dc=com", + "member: cn=t\\*,ou=people,dc=example,dc=com"); + result = conn.processOperation(addGroupTest); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addGroupTest2 = new AddRequest( + "dn: cn=Test\\<Group*2,ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: groupOfNames", + "cn: Test<Group*2", + "member: cn=t\\=,ou=people,dc=example,dc=com"); + result = conn.processOperation(addGroupTest2); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addGroupTest3 = new AddRequest( + "dn: cn=Test\\>Group*3,ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: groupOfNames", + "cn: Test>Group*3", + "member: cn=Test\\<Group*2,ou=people,dc=example,dc=com"); + result = conn.processOperation(addGroupTest3); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addPeopleSub = new AddRequest( + "dn: ou=s\\;ub,ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: organizationalUnit"); + result = conn.processOperation(addPeopleSub); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addUserTestSub = new AddRequest( + "dn: cn=testsub,ou=s\\;ub,ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: person", + "objectClass: organizationalPerson", + "cn: testsub", + "sn: Testsub", + "userPassword: test"); + result = conn.processOperation(addUserTestSub); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addGroupTest4 = new AddRequest( + "dn: cn=TestGroup4,ou=s\\;ub,ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: groupOfNames", + "cn: TestGroup4", + "member: cn=testsub,ou=s\\;ub,ou=people,dc=example,dc=com"); + result = conn.processOperation(addGroupTest4); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + } + } + + + @AfterClass + public static void destroyLDAP() { + ldapServer.shutDown(true); + } +} Index: apache-tomcat-9.0.36-src/webapps/docs/changelog.xml =================================================================== --- apache-tomcat-9.0.36-src.orig/webapps/docs/changelog.xml +++ apache-tomcat-9.0.36-src/webapps/docs/changelog.xml @@ -85,6 +85,10 @@ <bug>64871</bug>: Log a warning if Tomcat blocks access to a file because it uses symlinks. (markt) </add> + <scode> + Expand coverage of unit tests for JNDIRealm using the UnboundID LDAP SDK + for Java. (markt) + </scode> </changelog> </subsection> <subsection name="Coyote"> @@ -141,6 +145,10 @@ Make handling of OpenSSL read errors more robust when plain text data is reported to be available to read. (markt) </fix> + <fix> + <bug>65033</bug>: Fix JNDI realm error handling when connecting to a + failed server when pooling was not enabled. (remm) + </fix> </changelog> </subsection> <subsection name="Other"> @@ -236,6 +244,11 @@ <add> Improve validation of storage location when using FileStore. (markt) </add> + <fix> + JNDIRealm connections should only be created with the container + classloader as the thread context classloader, just like for the JAAS + realm. (remm) + </fix> </changelog> </subsection> <subsection name="Coyote"> @@ -3915,6 +3928,12 @@ subsequently incremented when using asynchronous processing over HTTP/2. (markt) </fix> + <fix> + Fix JNDIRealm pooling problems retrying on another bad connection. Any + retries are made on a new connection, just like with the single + connection scenario. Also remove all connections from the pool after + an error. (remm) + </fix> </changelog> </subsection> <subsection name="Jasper"> @@ -9657,6 +9676,9 @@ Update the internal fork of Commons Codec to r1725746 (1.9 plus additional fixes). (markt) </update> + <update> + Add connection pooling to JNDI realm. (remm) + </update> </changelog> </subsection> </section> Index: apache-tomcat-9.0.36-src/java/org/apache/catalina/realm/LocalStrings.properties =================================================================== --- apache-tomcat-9.0.36-src.orig/java/org/apache/catalina/realm/LocalStrings.properties +++ apache-tomcat-9.0.36-src/java/org/apache/catalina/realm/LocalStrings.properties @@ -77,6 +77,7 @@ jndiRealm.exception.retry=Exception perf jndiRealm.invalidHostnameVerifier=[{0}] not a valid class name for a HostnameVerifier jndiRealm.invalidSslProtocol=Given protocol [{0}] is invalid. It has to be one of [{1}] jndiRealm.invalidSslSocketFactory=[{0}] not a valid class name for an SSLSocketFactory +jndiRealm.multipleEntries=User name [{0}] has multiple entries jndiRealm.negotiatedTls=Negotiated tls connection using protocol [{0}] jndiRealm.open=Exception opening directory server connection jndiRealm.tlsClose=Exception closing tls response Index: apache-tomcat-9.0.36-src/test/org/apache/catalina/realm/TestJNDIRealm.java =================================================================== --- apache-tomcat-9.0.36-src.orig/test/org/apache/catalina/realm/TestJNDIRealm.java +++ apache-tomcat-9.0.36-src/test/org/apache/catalina/realm/TestJNDIRealm.java @@ -109,6 +109,27 @@ public class TestJNDIRealm { Assert.assertEquals(ha1(), ((GenericPrincipal)principal).getPassword()); } + volatile int count = 0; + + @Test + public void testErrorRealm() throws Exception { + Context context = new TesterContext(); + JNDIRealm realm = new JNDIRealm(); + realm.setContainer(context); + realm.setUserSearch(""); + // Connect to something that will fail + realm.setConnectionURL("ldap://127.0.0.1:12345"); + realm.start(); + + count = 0; + (new Thread(() -> { realm.authenticate("foo", "bar"); count++; })).start(); + (new Thread(() -> { realm.authenticate("foo", "bar"); count++; })).start(); + (new Thread(() -> { realm.authenticate("foo", "bar"); count++; })).start(); + Thread.sleep(10); + + Assert.assertEquals(3, count); + } + private JNDIRealm buildRealm(String password) throws javax.naming.NamingException, NoSuchFieldException, IllegalAccessException, LifecycleException { @@ -117,9 +138,12 @@ public class TestJNDIRealm { realm.setContainer(context); realm.setUserSearch(""); - Field field = JNDIRealm.class.getDeclaredField("context"); + // Usually everything is created in create() but that's not the case here + Field field = JNDIRealm.class.getDeclaredField("singleConnection"); field.setAccessible(true); - field.set(realm, mockDirContext(mockSearchResults(password))); + Field field2 = JNDIRealm.JNDIConnection.class.getDeclaredField("context"); + field2.setAccessible(true); + field2.set(field.get(realm), mockDirContext(mockSearchResults(password))); realm.start(); Index: apache-tomcat-9.0.36-src/test/org/apache/catalina/realm/TestJNDIRealmAttributeValueEscape.java =================================================================== --- /dev/null +++ apache-tomcat-9.0.36-src/test/org/apache/catalina/realm/TestJNDIRealmAttributeValueEscape.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina.realm; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +@RunWith(Parameterized.class) +public class TestJNDIRealmAttributeValueEscape { + + @Parameterized.Parameters(name = "{index}: in[{0}], out[{1}]") + public static Collection<Object[]> parameters() { + List<Object[]> parameterSets = new ArrayList<>(); + + // No escaping required + parameterSets.add(new String[] { "none", "none" }); + // Simple cases (same order as RFC 4512 section 2) + // Each appearing at the beginning, middle and ent + parameterSets.add(new String[] { " test", "\\20test" }); + parameterSets.add(new String[] { "te st", "te st" }); + parameterSets.add(new String[] { "test ", "test\\20" }); + parameterSets.add(new String[] { "#test", "\\23test" }); + parameterSets.add(new String[] { "te#st", "te#st" }); + parameterSets.add(new String[] { "test#", "test#" }); + parameterSets.add(new String[] { "\"test", "\\22test" }); + parameterSets.add(new String[] { "te\"st", "te\\22st" }); + parameterSets.add(new String[] { "test\"", "test\\22" }); + parameterSets.add(new String[] { "+test", "\\2Btest" }); + parameterSets.add(new String[] { "te+st", "te\\2Bst" }); + parameterSets.add(new String[] { "test+", "test\\2B" }); + parameterSets.add(new String[] { ",test", "\\2Ctest" }); + parameterSets.add(new String[] { "te,st", "te\\2Cst" }); + parameterSets.add(new String[] { "test,", "test\\2C" }); + parameterSets.add(new String[] { ";test", "\\3Btest" }); + parameterSets.add(new String[] { "te;st", "te\\3Bst" }); + parameterSets.add(new String[] { "test;", "test\\3B" }); + parameterSets.add(new String[] { "<test", "\\3Ctest" }); + parameterSets.add(new String[] { "te<st", "te\\3Cst" }); + parameterSets.add(new String[] { "test<", "test\\3C" }); + parameterSets.add(new String[] { ">test", "\\3Etest" }); + parameterSets.add(new String[] { "te>st", "te\\3Est" }); + parameterSets.add(new String[] { "test>", "test\\3E" }); + parameterSets.add(new String[] { "\\test", "\\5Ctest" }); + parameterSets.add(new String[] { "te\\st", "te\\5Cst" }); + parameterSets.add(new String[] { "test\\", "test\\5C" }); + parameterSets.add(new String[] { "\u0000test", "\\00test" }); + parameterSets.add(new String[] { "te\u0000st", "te\\00st" }); + parameterSets.add(new String[] { "test\u0000", "test\\00" }); + return parameterSets; + } + + + @Parameter(0) + public String in; + @Parameter(1) + public String out; + + private JNDIRealm realm = new JNDIRealm(); + + @Test + public void testConvertToHexEscape() throws Exception { + String result = realm.doAttributeValueEscaping(in); + Assert.assertEquals(out, result); + } +} Index: apache-tomcat-9.0.36-src/webapps/docs/config/realm.xml =================================================================== --- apache-tomcat-9.0.36-src.orig/webapps/docs/config/realm.xml +++ apache-tomcat-9.0.36-src/webapps/docs/config/realm.xml @@ -433,6 +433,13 @@ property.</p> </attribute> + <attribute name="connectionPoolSize" required="false"> + <p>The JNDI realm can use a pool of connections to the directory server + to avoid blocking on a single connection. This attribute value is the + maximum pool size. If not specified, it will use <code>1</code>, which + means a single connection will be used.</p> + </attribute> + <attribute name="connectionTimeout" required="false"> <p>The timeout in milliseconds to use when establishing the connection to the LDAP directory. If not specified, a value of 5000 (5 seconds) is @@ -622,6 +629,13 @@ specified, the default value of <code>302</code> is used.</p> </attribute> + <attribute name="useContextClassLoader" required="false"> + <p>Instructs JNDIRealm to use the context class loader when opening the + connection for the JNDI provider. The default value is + <code>true</code>. To load classes using the container's classloader, + specify <code>false</code>.</p> + </attribute> + <attribute name="useDelegatedCredential" required="false"> <p>When the JNDIRealm is used with the SPNEGO authenticator, delegated credentials for the user may be available. If such credentials are
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor