[jboss-cvs] JBossAS SVN: r74918 - projects/security/security-negotiation/trunk/jboss-negotiation/src/main/java/org/jboss/security/negotiation/prototype.

jboss-cvs-commits at lists.jboss.org jboss-cvs-commits at lists.jboss.org
Mon Jun 23 18:04:54 EDT 2008


Author: darran.lofthouse at jboss.com
Date: 2008-06-23 18:04:53 -0400 (Mon, 23 Jun 2008)
New Revision: 74918

Added:
   projects/security/security-negotiation/trunk/jboss-negotiation/src/main/java/org/jboss/security/negotiation/prototype/LdapExtLoginModule.java
Log:
Borrow existing login module.

Copied: projects/security/security-negotiation/trunk/jboss-negotiation/src/main/java/org/jboss/security/negotiation/prototype/LdapExtLoginModule.java (from rev 74917, projects/security/security-jboss-sx/trunk/jbosssx/src/main/org/jboss/security/auth/spi/LdapExtLoginModule.java)
===================================================================
--- projects/security/security-negotiation/trunk/jboss-negotiation/src/main/java/org/jboss/security/negotiation/prototype/LdapExtLoginModule.java	                        (rev 0)
+++ projects/security/security-negotiation/trunk/jboss-negotiation/src/main/java/org/jboss/security/negotiation/prototype/LdapExtLoginModule.java	2008-06-23 22:04:53 UTC (rev 74918)
@@ -0,0 +1,620 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2006, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.security.auth.spi;
+
+import java.security.Principal;
+import java.security.acl.Group;
+import java.util.Iterator;
+import java.util.Properties;
+import java.util.Map.Entry;
+
+import javax.management.ObjectName;
+import javax.naming.Context;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import javax.naming.ldap.InitialLdapContext;
+import javax.security.auth.login.LoginException;
+
+import org.jboss.security.SimpleGroup;
+
+/**
+ The org.jboss.security.auth.spi.LdapExtLoginModule, added in jboss-4.0.3, is an
+ alternate ldap login module implementation that uses searches for locating both
+ the user to bind as for authentication as well as the associated roles. The
+ roles query will recursively follow distinguished names (DNs) to navigate a
+ hierarchical role structure.
+
+ The LoginModule options include whatever options your LDAP JNDI provider
+ supports. Examples of standard property names are:
+
+ * Context.INITIAL_CONTEXT_FACTORY = "java.naming.factory.initial"
+ * Context.SECURITY_PROTOCOL = "java.naming.security.protocol"
+ * Context.PROVIDER_URL = "java.naming.provider.url"
+ * Context.SECURITY_AUTHENTICATION = "java.naming.security.authentication"
+
+ The authentication happens in 2 steps:
+ # An initial bind to the ldap server is done using the __bindDN__ and
+ __bindCredential__ options. The __bindDN__ is some user with the ability to
+ search both the __baseDN__ and __rolesCtxDN__ trees for the user and roles. The
+ user DN to authenticate against is queried using the filter specified by the
+ __baseFilter__ attribute (see the __baseFilter__ option description for its
+ syntax). 
+ # The resulting user DN is then authenticated by binding to ldap server using
+ the user DN as the InitialLdapContext environment Context.SECURITY_PRINCIPAL.
+
+ The Context.SECURITY_CREDENTIALS property is either set to the String password
+ obtained by the callback handler.
+
+ If this is successful, the associated user roles are queried using the
+ __rolesCtxDN__, __roleAttributeID__, __roleAttributeIsDN__,
+ __roleNameAttributeID__, and __roleFilter__ options.
+
+ The full odule properties include:
+ * __baseCtxDN__ : The fixed DN of the context to start the user search from.
+ * __bindDN__ : The DN used to bind against the ldap server for the user and
+ roles queries. This is some DN with read/search permissions on the baseCtxDN and
+ rolesCtxDN values.
+ * __bindCredential__ : The password for the bindDN. This can be encrypted if the
+ jaasSecurityDomain is specified.
+ * __jaasSecurityDomain__ : The JMX ObjectName of the JaasSecurityDomain to use
+ to decrypt the java.naming.security.principal. The encrypted form of the
+ password is that returned by the JaasSecurityDomain#encrypt64(byte[]) method.
+ The org.jboss.security.plugins.PBEUtils can also be used to generate the
+ encrypted form.
+ * __baseFilter__ : A search filter used to locate the context of the user to
+ authenticate. The input username/userDN as obtained from the login module
+ callback will be substituted into the filter anywhere a "{0}" expression is
+ seen. This substituion behavior comes from the standard
+ __DirContext.search(Name, String, Object[], SearchControls cons)__ method. An
+ common example search filter is "(uid={0})".
+ * __rolesCtxDN__ : The fixed DN of the context to search for user roles.
+ Consider that this is not the Distinguished Name of where the actual roles are;
+ rather, this is the DN of where the objects containing the user roles are (e.g.
+ for active directory, this is the DN where the user account is)
+ * __roleFilter__ : A search filter used to locate the roles associated with the
+ authenticated user. The input username/userDN as obtained from the login module
+ callback will be substituted into the filter anywhere a "{0}" expression is
+ seen. The authenticated userDN will be substituted into the filter anywhere a
+ "{1}" is seen.  An example search filter that matches on the input username is:
+ "(member={0})". An alternative that matches on the authenticated userDN is:
+ "(member={1})".
+ * __roleAttributeIsDN__ : A flag indicating whether the user's role attribute
+ contains the fully distinguished name of a role object, or the users's role
+ attribute contains the role name. If false, the role name is taken from the
+ value of the user's role attribute. If true, the role attribute represents the
+ distinguished name of a role object.  The role name is taken from the value of
+ the roleNameAttributeId` attribute of the corresponding object.  In certain
+ directory schemas (e.g., Microsoft Active Directory), role (group)attributes in
+ the user object are stored as DNs to role objects instead of as simple names, in
+ which case, this property should be set to true. The default value of this
+ property is false.
+ * __roleNameAttributeID__ : The name of the attribute of the role object which
+ corresponds to the name of the role.  If the __roleAttributeIsDN__ property is
+ set to true, this property is used to find the role object's name attribute. If
+ the __roleAttributeIsDN__ property is set to false, this property is ignored.
+ * __roleRecursion__ : How deep the role search will go below a given matching
+ context. Disable with 0, which is the default.
+ * __searchTimeLimit__ : The timeout in milliseconds for the user/role searches.
+ Defaults to 10000 (10 seconds).
+ * __searchScope__ : Sets the search scope to one of the strings. The default is
+ SUBTREE_SCOPE.
+ ** OBJECT_SCOPE : only search the named roles context.
+ ** ONELEVEL_SCOPE : search directly under the named roles context.
+ ** SUBTREE_SCOPE :  If the roles context is not a DirContext, search only the
+ object. If the roles context is a DirContext, search the subtree rooted at the
+ named object, including the named object itself
+ * __allowEmptyPasswords__ : A flag indicating if empty(length==0) passwords
+ should be passed to the ldap server. An empty password is treated as an
+ anonymous login by some ldap servers and this may not be a desirable feature.
+ Set this to false to reject empty passwords, true to have the ldap server
+ validate the empty password. The default is true.
+ 
+ @author Andy Oliver
+ @author Scott.Stark at jboss.org
+ @version $Revision$ */
+public class LdapExtLoginModule extends UsernamePasswordLoginModule
+{
+   private static final String ROLES_CTX_DN_OPT = "rolesCtxDN";
+
+   private static final String ROLE_ATTRIBUTE_ID_OPT = "roleAttributeID";
+
+   private static final String ROLE_ATTRIBUTE_IS_DN_OPT = "roleAttributeIsDN";
+
+   private static final String ROLE_NAME_ATTRIBUTE_ID_OPT = "roleNameAttributeID";
+
+   private static final String BIND_DN = "bindDN";
+
+   private static final String BIND_CREDENTIAL = "bindCredential";
+
+   private static final String BASE_CTX_DN = "baseCtxDN";
+
+   private static final String BASE_FILTER_OPT = "baseFilter";
+
+   private static final String ROLE_FILTER_OPT = "roleFilter";
+
+   private static final String ROLE_RECURSION = "roleRecursion";
+
+   private static final String DEFAULT_ROLE = "defaultRole";
+
+   private static final String SEARCH_TIME_LIMIT_OPT = "searchTimeLimit";
+
+   private static final String SEARCH_SCOPE_OPT = "searchScope";
+
+   private static final String SECURITY_DOMAIN_OPT = "jaasSecurityDomain";
+
+   protected String bindDN;
+
+   protected String bindCredential;
+
+   protected String baseDN;
+
+   protected String baseFilter;
+
+   protected String rolesCtxDN;
+
+   protected String roleFilter;
+
+   protected String roleAttributeID;
+
+   protected String roleNameAttributeID;
+
+   protected boolean roleAttributeIsDN;
+
+   protected int recursion = 0;
+
+   protected int searchTimeLimit = 10000;
+
+   protected int searchScope = SearchControls.SUBTREE_SCOPE;
+
+   protected boolean trace;
+   
+   // simple flag to indicate is the validatePassword method was called
+   protected boolean isPasswordValidated = false;
+
+   public LdapExtLoginModule()
+   {
+   }
+
+   private transient SimpleGroup userRoles = new SimpleGroup("Roles");
+
+   /**
+    Overridden to return an empty password string as typically one cannot obtain a
+    user's password. We also override the validatePassword so this is ok.
+    @return and empty password String
+    */
+   protected String getUsersPassword() throws LoginException
+   {
+      return "";
+   }
+
+   /**
+    Overridden by subclasses to return the Groups that correspond to the to the
+    role sets assigned to the user. Subclasses should create at least a Group
+    named "Roles" that contains the roles assigned to the user. A second common
+    group is "CallerPrincipal" that provides the application identity of the user
+    rather than the security domain identity.
+    @return Group[] containing the sets of roles
+    */
+   protected Group[] getRoleSets() throws LoginException
+   {
+      // SECURITY-225: check if authentication was already done in a previous login module
+      // and perform role mapping
+      if (!isPasswordValidated)
+      {
+         try
+         {
+            String username = getUsername();
+            createLdapInitContext(username, null);
+            defaultRole();
+         }
+         catch (Exception e)
+         {
+            LoginException le = new LoginException();
+            le.initCause(e);
+            throw le;
+         }
+      }
+
+      Group[] roleSets = {userRoles};
+      return roleSets;
+   }
+
+   /**
+    Validate the inputPassword by creating a LDAP InitialContext with the
+    SECURITY_CREDENTIALS set to the password.
+    @param inputPassword the password to validate.
+    @param expectedPassword ignored
+    */
+   protected boolean validatePassword(String inputPassword, String expectedPassword)
+   {
+      isPasswordValidated = true;
+      boolean isValid = false;
+      if (inputPassword != null)
+      {
+         // See if this is an empty password that should be disallowed
+         if (inputPassword.length() == 0)
+         {
+            // Check for an allowEmptyPasswords option
+            boolean allowEmptyPasswords = true;
+            String flag = (String) options.get("allowEmptyPasswords");
+            if (flag != null)
+               allowEmptyPasswords = Boolean.valueOf(flag).booleanValue();
+            if (allowEmptyPasswords == false)
+            {
+               log.trace("Rejecting empty password due to allowEmptyPasswords");
+               return false;
+            }
+         }
+
+         try
+         {
+            // Validate the password by trying to create an initial context
+            String username = getUsername();
+            isValid = createLdapInitContext(username, inputPassword);
+            defaultRole();
+            isValid = true;
+         }
+         catch (Throwable e)
+         {
+            super.setValidateError(e);
+         }
+      }
+      return isValid;
+   }
+
+   /**
+    @todo move to a generic role mapping function at the base login module
+    */
+   private void defaultRole()
+   {
+      try
+      {
+         String defaultRole = (String) options.get(DEFAULT_ROLE);
+         if (defaultRole == null || defaultRole.equals(""))
+         {
+            return;
+         }
+         Principal p = super.createIdentity(defaultRole);
+         log.trace("Assign user to role " + defaultRole);
+         userRoles.addMember(p);
+      }
+      catch (Exception e)
+      {
+         super.log.debug("could not add default role to user", e);
+      }
+   }
+
+   /**
+    Bind to the LDAP server for authentication. 
+    
+    @param username
+    @param credential
+    @return true if the bind for authentication succeeded
+    @throws NamingException
+    */
+   private boolean createLdapInitContext(String username, Object credential) throws Exception
+   {
+      bindDN = (String) options.get(BIND_DN);
+      bindCredential = (String) options.get(BIND_CREDENTIAL);
+      String securityDomain = (String) options.get(SECURITY_DOMAIN_OPT);
+      if (securityDomain != null)
+      {
+         ObjectName serviceName = new ObjectName(securityDomain);
+         char[] tmp = DecodeAction.decode(bindCredential, serviceName);
+         bindCredential = new String(tmp);
+      }
+
+      baseDN = (String) options.get(BASE_CTX_DN);
+      baseFilter = (String) options.get(BASE_FILTER_OPT);
+      roleFilter = (String) options.get(ROLE_FILTER_OPT);
+      roleAttributeID = (String) options.get(ROLE_ATTRIBUTE_ID_OPT);
+      if (roleAttributeID == null)
+         roleAttributeID = "role";
+      // Is user's role attribute a DN or the role name
+      String roleAttributeIsDNOption = (String) options.get(ROLE_ATTRIBUTE_IS_DN_OPT);
+      roleAttributeIsDN = Boolean.valueOf(roleAttributeIsDNOption).booleanValue();
+      roleNameAttributeID = (String) options.get(ROLE_NAME_ATTRIBUTE_ID_OPT);
+      if (roleNameAttributeID == null)
+         roleNameAttributeID = "name";
+      rolesCtxDN = (String) options.get(ROLES_CTX_DN_OPT);
+      String strRecursion = (String) options.get(ROLE_RECURSION);
+      try
+      {
+         recursion = Integer.parseInt(strRecursion);
+      }
+      catch (Exception e)
+      {
+         if (trace)
+            log.trace("Failed to parse: " + strRecursion + ", disabling recursion");
+         // its okay for this to be 0 as this just disables recursion
+         recursion = 0;
+      }
+      String timeLimit = (String) options.get(SEARCH_TIME_LIMIT_OPT);
+      if (timeLimit != null)
+      {
+         try
+         {
+            searchTimeLimit = Integer.parseInt(timeLimit);
+         }
+         catch (NumberFormatException e)
+         {
+            if (trace)
+               log.trace("Failed to parse: " + timeLimit + ", using searchTimeLimit=" + searchTimeLimit);
+         }
+      }
+      String scope = (String) options.get(SEARCH_SCOPE_OPT);
+      if ("OBJECT_SCOPE".equalsIgnoreCase(scope))
+         searchScope = SearchControls.OBJECT_SCOPE;
+      else if ("ONELEVEL_SCOPE".equalsIgnoreCase(scope))
+         searchScope = SearchControls.ONELEVEL_SCOPE;
+      if ("SUBTREE_SCOPE".equalsIgnoreCase(scope))
+         searchScope = SearchControls.SUBTREE_SCOPE;
+
+      // Get the admin context for searching
+      InitialLdapContext ctx = null;
+      try
+      {
+         ctx = constructInitialLdapContext(bindDN, bindCredential);
+         // Validate the user by binding against the userDN
+         String userDN = bindDNAuthentication(ctx, username, credential, baseDN, baseFilter);
+
+         // Query for roles matching the role filter
+         SearchControls constraints = new SearchControls();
+         constraints.setSearchScope(searchScope);
+         constraints.setReturningAttributes(new String[0]);
+         constraints.setTimeLimit(searchTimeLimit);
+         rolesSearch(ctx, constraints, username, userDN, recursion, 0);
+      }
+      finally
+      {
+         if (ctx != null)
+            ctx.close();
+      }
+      return true;
+   }
+
+   /**
+    @param ctx - the context to search from
+    @param user - the input username
+    @param credential - the bind credential
+    @param baseDN - base DN to search the ctx from
+    @param filter - the search filter string
+    @return the userDN string for the successful authentication 
+    @throws NamingException
+    */
+   @SuppressWarnings("unchecked")
+   protected String bindDNAuthentication(InitialLdapContext ctx, String user, Object credential, String baseDN,
+         String filter) throws NamingException
+   {
+      SearchControls constraints = new SearchControls();
+      constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
+      constraints.setReturningAttributes(new String[0]);
+      constraints.setTimeLimit(searchTimeLimit);
+
+      NamingEnumeration results = null;
+
+      Object[] filterArgs = {user};
+      results = ctx.search(baseDN, filter, filterArgs, constraints);
+      if (results.hasMore() == false)
+      {
+         results.close();
+         throw new NamingException("Search of baseDN(" + baseDN + ") found no matches");
+      }
+
+      SearchResult sr = (SearchResult) results.next();
+      String name = sr.getName();
+      String userDN = null;
+      if (sr.isRelative() == true)
+         userDN = name + "," + baseDN;
+      else
+         throw new NamingException("Can't follow referal for authentication: " + name);
+
+      results.close();
+      results = null;
+      // SECURITY-225: don't need to authenticate again
+      if (isPasswordValidated)
+      {
+         // Bind as the user dn to authenticate the user
+         InitialLdapContext userCtx = constructInitialLdapContext(userDN, credential);
+         userCtx.close();
+      }
+
+      return userDN;
+   }
+
+   /**
+    @param ctx
+    @param constraints
+    @param user
+    @param userDN
+    @param recursionMax
+    @param nesting
+    @throws NamingException
+    */
+   @SuppressWarnings("unchecked")
+   protected void rolesSearch(InitialLdapContext ctx, SearchControls constraints, String user, String userDN,
+         int recursionMax, int nesting) throws NamingException
+   {
+      Object[] filterArgs = {user, userDN};
+      NamingEnumeration results = ctx.search(rolesCtxDN, roleFilter, filterArgs, constraints);
+      try
+      {
+         while (results.hasMore())
+         {
+            SearchResult sr = (SearchResult) results.next();
+            String dn = canonicalize(sr.getName());
+            if (nesting == 0 && roleAttributeIsDN && roleNameAttributeID != null)
+            {
+               // Check the top context for role names
+               String[] attrNames = {roleNameAttributeID};
+               Attributes result2 = ctx.getAttributes(dn, attrNames);
+               Attribute roles2 = result2.get(roleNameAttributeID);
+               if (roles2 != null)
+               {
+                  for (int m = 0; m < roles2.size(); m++)
+                  {
+                     String roleName = (String) roles2.get(m);
+                     addRole(roleName);
+                  }
+               }
+            }
+
+            // Query the context for the roleDN values
+            String[] attrNames = {roleAttributeID};
+            Attributes result = ctx.getAttributes(dn, attrNames);
+            if (result != null && result.size() > 0)
+            {
+               Attribute roles = result.get(roleAttributeID);
+               for (int n = 0; n < roles.size(); n++)
+               {
+                  String roleName = (String) roles.get(n);
+                  if (roleAttributeIsDN)
+                  {
+                     // Query the roleDN location for the value of roleNameAttributeID
+                     String roleDN = roleName;
+                     String[] returnAttribute = {roleNameAttributeID};
+                     log.trace("Using roleDN: " + roleDN);
+                     try
+                     {
+                        Attributes result2 = ctx.getAttributes(roleDN, returnAttribute);
+                        Attribute roles2 = result2.get(roleNameAttributeID);
+                        if (roles2 != null)
+                        {
+                           for (int m = 0; m < roles2.size(); m++)
+                           {
+                              roleName = (String) roles2.get(m);
+                              addRole(roleName);
+                           }
+                        }
+                     }
+                     catch (NamingException e)
+                     {
+                        log.trace("Failed to query roleNameAttrName", e);
+                     }
+                  }
+                  else
+                  {
+                     // The role attribute value is the role name
+                     addRole(roleName);
+                  }
+               }
+            }
+
+            if (nesting < recursionMax)
+            {
+               rolesSearch(ctx, constraints, user, dn, recursionMax, nesting + 1);
+            }
+         }
+      }
+      finally
+      {
+         if (results != null)
+            results.close();
+      }
+
+   }
+
+   @SuppressWarnings("unchecked")
+   private InitialLdapContext constructInitialLdapContext(String dn, Object credential) throws NamingException
+   {
+      Properties env = new Properties();
+      Iterator iter = options.entrySet().iterator();
+      while (iter.hasNext())
+      {
+         Entry entry = (Entry) iter.next();
+         env.put(entry.getKey(), entry.getValue());
+      }
+
+      // Set defaults for key values if they are missing
+      String factoryName = env.getProperty(Context.INITIAL_CONTEXT_FACTORY);
+      if (factoryName == null)
+      {
+         factoryName = "com.sun.jndi.ldap.LdapCtxFactory";
+         env.setProperty(Context.INITIAL_CONTEXT_FACTORY, factoryName);
+      }
+      String authType = env.getProperty(Context.SECURITY_AUTHENTICATION);
+      if (authType == null)
+         env.setProperty(Context.SECURITY_AUTHENTICATION, "simple");
+      String protocol = env.getProperty(Context.SECURITY_PROTOCOL);
+      String providerURL = (String) options.get(Context.PROVIDER_URL);
+      if (providerURL == null)
+         providerURL = "ldap://localhost:" + ((protocol != null && protocol.equals("ssl")) ? "636" : "389");
+
+      env.setProperty(Context.PROVIDER_URL, providerURL);
+      // JBAS-3555, allow anonymous login with no bindDN and bindCredential
+      if (dn != null)
+         env.setProperty(Context.SECURITY_PRINCIPAL, dn);
+      if (credential != null)
+         env.put(Context.SECURITY_CREDENTIALS, credential);
+      traceLdapEnv(env);
+      return new InitialLdapContext(env, null);
+   }
+
+   private void traceLdapEnv(Properties env)
+   {
+      if (trace)
+      {
+         Properties tmp = new Properties();
+         tmp.putAll(env);
+         tmp.setProperty(Context.SECURITY_CREDENTIALS, "***");
+         log.trace("Logging into LDAP server, env=" + tmp.toString());
+      }
+   }
+
+   //JBAS-3438 : Handle "/" correctly
+   private String canonicalize(String searchResult)
+   {
+      String result = searchResult;
+      int len = searchResult.length();
+
+      if (searchResult.endsWith("\""))
+      {
+         result = searchResult.substring(0, len - 1) + "," + rolesCtxDN + "\"";
+      }
+      else
+      {
+         result = searchResult + "," + rolesCtxDN;
+      }
+      return result;
+   }
+
+   private void addRole(String roleName)
+   {
+      if (roleName != null)
+      {
+         try
+         {
+            Principal p = super.createIdentity(roleName);
+            log.trace("Assign user to role " + roleName);
+            userRoles.addMember(p);
+         }
+         catch (Exception e)
+         {
+            log.debug("Failed to create principal: " + roleName, e);
+         }
+      }
+   }
+}




More information about the jboss-cvs-commits mailing list