| /* |
| * Copyright (c) 2003, 2021 Oracle and/or its affiliates. All rights reserved. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0, which is available at |
| * http://www.eclipse.org/legal/epl-2.0. |
| * |
| * This Source Code may also be made available under the following Secondary |
| * Licenses when the conditions for such availability set forth in the |
| * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, |
| * version 2 with the GNU Classpath Exception, which is available at |
| * https://www.gnu.org/software/classpath/license.html. |
| * |
| * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 |
| */ |
| |
| package com.sun.jndi.ldap.obj; |
| |
| import com.sun.enterprise.security.GroupPrincipal; |
| import com.sun.jndi.ldap.LdapName; |
| import com.sun.jndi.ldap.LdapURL; |
| |
| import java.security.Principal; |
| import java.util.ArrayList; |
| import java.util.Enumeration; |
| import java.util.Hashtable; |
| import java.util.Iterator; |
| import java.util.NoSuchElementException; |
| import java.util.Set; |
| |
| import javax.naming.InvalidNameException; |
| import javax.naming.Name; |
| import javax.naming.NameNotFoundException; |
| import javax.naming.NamingEnumeration; |
| import javax.naming.NamingException; |
| import javax.naming.directory.Attribute; |
| import javax.naming.directory.Attributes; |
| import javax.naming.directory.BasicAttribute; |
| import javax.naming.directory.BasicAttributes; |
| import javax.naming.directory.DirContext; |
| import javax.naming.directory.ModificationItem; |
| import javax.naming.directory.SearchControls; |
| import javax.naming.directory.SearchResult; |
| import javax.naming.ldap.LdapContext; |
| import javax.naming.spi.NamingManager; |
| |
| /** |
| * A representation of the LDAP groupOfURLs object class. |
| * This is a dynamic group: its membership is determined by evaluating the |
| * group's LDAP URLs. |
| * <p> |
| * Note that when a <tt>GroupOfURLs</tt> object is created by the application |
| * program then most of its methods throw {@link IllegalStateException} |
| * until the program binds the object in the directory. However, when a |
| * <tt>GroupOfURLs</tt> object is returned to the application program then the |
| * object is already bound in the directory and its methods function normally. |
| * <p> |
| * A <tt>GroupOfURLs</tt> instance is not synchronized against concurrent |
| * multithreaded access. Multiple threads trying to access and modify a |
| * <tt>GroupOfURLs</tt> should lock the object. |
| * <p> |
| * In order to bind a <tt>GroupOfURLs</tt> object in the directory, the |
| * following LDAP object class definition must be supported in the directory |
| * schema: |
| * <pre> |
| * ( 2.16.840.1.113730.3.2.33 NAME 'groupOfURLs' |
| * SUP top |
| * STRUCTURAL |
| * MUST cn |
| * MAY ( memberURL $ |
| * businessCategory $ |
| * description $ |
| * o $ |
| * ou $ |
| * owner $ |
| * seeAlso ) ) |
| * </pre> |
| * See |
| * {@link javax.naming.directory.DirContext#bind(javax.naming.Name, |
| * java.lang.Object, javax.naming.directory.Attributes) DirContext.bind} |
| * for details on binding an object in the directory. |
| * <p> |
| * The following code sample shows how the class may be used: |
| * <pre> |
| * |
| * // set the java.naming.factory.object property |
| * env.put(Context.OBJECT_FACTORIES, |
| * "com.sun.jndi.ldap.obj.LdapGroupFactory"); |
| * |
| * // set the java.naming.factory.state property |
| * env.put(Context.STATE_FACTORIES, |
| * "com.sun.jndi.ldap.obj.LdapGroupFactory"); |
| * |
| * // create an initial context using the supplied environment properties |
| * DirContext ctx = new InitialDirContext(env); |
| * |
| * // create a set of member URLs |
| * Set members = new HashSet(); |
| * members.add( |
| * "ldap:///" + ctx.getNameInNamespace() + "??sub?(title=Manager)"); |
| * Group managers = new GroupOfURLs(members); |
| * |
| * // bind the group in the directory |
| * ctx.bind("cn=managers,ou=groups", managers); |
| * |
| * // list all of the group's members |
| * listMembers(managers); |
| * ... |
| * |
| * |
| * // list the members of a group (subgroups are expanded, by default) |
| * void listMembers(Group group) { |
| * for (Enumeration members = group.members(); |
| * members.hasMoreElements(); ) { |
| * Object object = members.nextElement(); |
| * if (object instanceof Group) { |
| * System.out.println("+" + ((Group) object).getName()); |
| * } else if (object instanceof Principal) { |
| * System.out.println(" " + ((Principal) object).getName()); |
| * } |
| * } |
| * } |
| * |
| * </pre> |
| * |
| * @author Vincent Ryan |
| */ |
| public class GroupOfURLs implements GroupPrincipal { |
| |
| private static final boolean debug = false; |
| private static final String OBJECT_CLASS = "groupOfURLs"; |
| private static final String MEMBER_ATTR_ID = "memberURL"; |
| private static final String EXPAND_GROUP = "com.sun.jndi.ldap.obj.expandGroup"; |
| private static final String GROUPS_ONLY = "(|(objectClass=groupOfNames)(objectClass=groupOfURLs))"; |
| private static final Attribute OBJECT_CLASS_ATTR = new BasicAttribute("objectClass", "top"); |
| static { |
| OBJECT_CLASS_ATTR.add(OBJECT_CLASS); |
| } |
| |
| private boolean objectIsBound; |
| private boolean expandGroup = true; |
| private Attributes attributes = null; |
| private Attribute memberAttr = null; |
| private SearchControls searchNoAttrs = null; |
| private SearchControls objectSearch = null; |
| private ModificationItem[] modification = new ModificationItem[1]; |
| |
| private String groupDN = null; |
| private String bindDN = null; |
| private DirContext rootCtx = null; |
| private DirContext ctx = null; |
| private DirContext bindCtx = null; |
| private Name name = null; |
| private Name bindName = null; |
| private Hashtable env = null; |
| |
| /** |
| * Create an empty group object. |
| * <p> |
| * Note that the newly constructed object does not represent a group in |
| * the directory until it is bound by using |
| * {@link javax.naming.directory.DirContext#bind(javax.naming.Name, |
| * java.lang.Object, javax.naming.directory.Attributes) DirContext.bind}. |
| */ |
| public GroupOfURLs() { |
| if (debug) { |
| System.out.println("[debug] constructing an empty group"); |
| } |
| objectIsBound = false; |
| // initialize the group's attribute set |
| attributes = new BasicAttributes(true); |
| attributes.put(OBJECT_CLASS_ATTR); |
| } |
| |
| |
| /** |
| * Create a group object with an initial set of member URLs. |
| * <p> |
| * Note that the newly constructed object does not represent a group in |
| * the directory until it is bound by using |
| * {@link javax.naming.directory.DirContext#bind(javax.naming.Name, |
| * java.lang.Object, javax.naming.directory.Attributes) DirContext.bind}. |
| * |
| * @param memberURLs The set of initial member URLs. It may be null. |
| * Each element is a string LDAP URL (RFC 2255). |
| */ |
| public GroupOfURLs(Set memberURLs) { |
| if (debug) { |
| System.out.println("[debug] constructing a group"); |
| } |
| objectIsBound = false; |
| // initialize the group's attribute set |
| attributes = new BasicAttributes(true); |
| attributes.put(OBJECT_CLASS_ATTR); |
| if (memberURLs != null && (!memberURLs.isEmpty())) { |
| memberAttr = new BasicAttribute(MEMBER_ATTR_ID); |
| for (Iterator i = memberURLs.iterator(); i.hasNext();) { |
| memberAttr.add(i.next()); |
| } |
| attributes.put(memberAttr); |
| } |
| } |
| |
| /** |
| * Create a group object from its entry in the directory. |
| */ |
| private GroupOfURLs(String groupDN, DirContext ctx, Name name, Hashtable env, Attributes attributes) { |
| initializeBoundState(groupDN, ctx, name, env, attributes); |
| } |
| |
| /** |
| * Create a group object from its entry in the directory. |
| * This method is called by {@link LdapGroupFactory}. |
| * |
| * @param groupDN The group's distinguished name. |
| * @param ctx An LDAP context. |
| * @param name The group's name relative to the context. |
| * @param env The context's environment properties. |
| * @param attributes The group's LDAP attributes. |
| * @return Object The new object instance. |
| */ |
| // package private (used by LdapGroupFactory) |
| static Object getObjectInstance(String groupDN, DirContext ctx, Name name, Hashtable env, Attributes attributes) { |
| if (debug) { |
| System.out.println("[debug] creating a group named: " + groupDN); |
| } |
| return new GroupOfURLs(groupDN, ctx, name, env, attributes); |
| } |
| |
| |
| /** |
| * A member cannot be added to the group directly. |
| * Instead, members are added by adding a group LDAP URL. |
| * See {@link #addMembers(String)} |
| * |
| * @param member The name of the member to be added. |
| * @return The exception is always thrown. |
| * @throws UnsupportedOperationException A member cannot be added directly. |
| */ |
| public boolean addMember(Principal member) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * Adds members to the group. |
| * Performs an LDAP modify to add the LDAP URL. |
| * |
| * @param members The string LDAP URL describing the members to be added. |
| * @throws NamingException The exception is thrown if an error occurs |
| * while performing LDAP modify. |
| * @throws IllegalStateException The exception is thrown if the |
| * group does not represent a group in the directory. |
| */ |
| public void addMembers(String members) throws NamingException { |
| if (!isBound()) { |
| throw new IllegalStateException(); |
| } |
| if (debug) { |
| System.out.println("[debug] adding the members: " + members); |
| } |
| modifyMembers(members, DirContext.ADD_ATTRIBUTE); |
| } |
| |
| /** |
| * Checks if the supplied name is a member of the group. |
| * Performs an LDAP search to determine membership. |
| * <p> |
| * By default, subgroups are also checked. |
| * As subgroup expansion is potentially an expensive activity the feature |
| * may be disabled by setting the environment property |
| * "com.sun.jndi.ldap.obj.expandGroup" |
| * to the string value "false. |
| * |
| * @param member The name of the member to be checked. |
| * @return boolean true if membership is confirmed; false otherwise. |
| * @throws IllegalStateException The exception is thrown if the |
| * group does not represent a group in the directory. |
| */ |
| public boolean isMember(Principal member) { |
| try { |
| return isMember(member.getName()); |
| } catch (NamingException e) { |
| if (debug) { |
| System.out.println("[debug] error testing for membership: " + e); |
| } |
| return false; |
| } |
| } |
| |
| |
| /** |
| * Checks if the supplied name is a member of the group. |
| * Performs an LDAP search to determine membership. |
| * <p> |
| * By default, subgroups are also checked. |
| * As subgroup expansion is potentially an expensive activity the feature |
| * may be disabled by setting the environment property |
| * "com.sun.jndi.ldap.obj.expandGroup" |
| * to the string value "false. |
| * |
| * @param dn The distinguished name (RFC 2253) of the member to be checked. |
| * @return boolean true if membership is confirmed; false otherwise. |
| * @throws NamingException The exception is thrown if an error occurs |
| * while performing LDAP search. |
| * @throws IllegalStateException The exception is thrown if the |
| * group does not represent a group in the directory. |
| */ |
| public boolean isMember(String dn) throws NamingException { |
| if (!isBound()) { |
| throw new IllegalStateException(); |
| } |
| if (debug) { |
| System.out.println("[debug] checking if \"" + dn + "\" is a member"); |
| } |
| |
| // Retrieve the group's member attribute unless already cached |
| if ((memberAttr != null) |
| || (memberAttr = ctx.getAttributes(name, new String[] {MEMBER_ATTR_ID}).get(MEMBER_ATTR_ID)) != null) { |
| |
| if (searchNoAttrs == null) { |
| searchNoAttrs = new SearchControls(); |
| // Request no attributes |
| searchNoAttrs.setReturningAttributes(new String[0]); |
| } |
| |
| // Process each LDAP URL |
| for (Enumeration values = memberAttr.getAll(); values.hasMoreElements();) { |
| |
| NamingEnumeration results = searchUsingLdapUrl((String) values.nextElement(), searchNoAttrs, dn); |
| |
| // Membership is confirmed if any results are returned |
| if (results != null && results.hasMore()) { |
| results.close(); // cleanup |
| return true; |
| } |
| } |
| |
| // Check subgroups |
| if (expandGroup) { |
| return isSubgroupMember(dn); |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Returns the members of the group. |
| * Performs LDAP searches to retrieve the members. |
| * <p> |
| * By default, subgroups and their members are also included. |
| * As subgroup expansion is potentially an expensive activity the feature |
| * may be disabled by setting the environment property |
| * "com.sun.jndi.ldap.obj.expandGroup" |
| * to the string value "false. When the feature is disabled only the group's |
| * direct members are returned. |
| * |
| * @return Enumeration The list of members of the group. |
| * When only the {@link LdapGroupFactory} object factory is active |
| * then each element in the enumeration is of class |
| * {@link com.sun.enterprise.security.GroupPrincipal} or |
| * {@link java.security.Principal} However, when additional |
| * object factories are active then the enumeration may contain |
| * elements of a different class. |
| * @throws IllegalStateException The exception is thrown if the |
| * group does not represent a group in the directory. |
| */ |
| public Enumeration members() { |
| try { |
| return members(null); |
| } catch (NamingException e) { |
| if (debug) { |
| System.out.println("[debug] error enumerating the members: " + e); |
| } |
| return new Members(); // empty |
| } |
| } |
| |
| /** |
| * Returns the members of the group that satisfy the search filter. |
| * Performs LDAP searches to retrieve the members. |
| * <p> |
| * By default, subgroups and their members are also included. |
| * As subgroup expansion is potentially an expensive activity the feature |
| * may be disabled by setting the environment property |
| * "com.sun.jndi.ldap.obj.expandGroup" |
| * to the string value "false. When the feature is disabled only the group's |
| * direct members are returned. |
| * |
| * @param filter The string filter to apply to the members of the group. |
| * If the argument is null then no filtering is performed. |
| * @return Enumeration The list of members that satisfy the filter. |
| * When only the {@link LdapGroupFactory} object factory is active |
| * then each element in the enumeration is of class |
| * {@link com.sun.enterprise.security.GroupPrincipal} or |
| * {@link java.security.Principal} However, when additional |
| * object factories are active then the enumeration may contain |
| * elements of a different class. |
| * @throws NamingException The exception is thrown if an error occurs |
| * while performing LDAP search. |
| * @throws IllegalStateException The exception is thrown if the |
| * group does not represent a group in the directory. |
| */ |
| public Enumeration members(String filter) throws NamingException { |
| if (!isBound()) { |
| throw new IllegalStateException(); |
| } |
| if (debug) { |
| System.out.println("[debug] enumerating the members"); |
| } |
| |
| // Retrieve the group's member attribute unless already cached |
| if ((memberAttr != null) |
| || (memberAttr = ctx.getAttributes(name, new String[] {MEMBER_ATTR_ID}).get(MEMBER_ATTR_ID)) != null) { |
| |
| if (objectSearch == null) { |
| objectSearch = new SearchControls(); |
| // Request objects |
| objectSearch.setReturningObjFlag(true); |
| } |
| return new Members(memberAttr.getAll(), filter, objectSearch); |
| } |
| |
| return new Members(); // empty |
| } |
| |
| /** |
| * A member cannot be removed from the group directly. |
| * Instead, members are removed by removing a group LDAP URL. |
| * See {@link #removeMembers(String)} |
| * |
| * @param member The name of the member to be removed. |
| * @return The exception is always thrown. |
| * @throws UnsupportedOperationException A member cannot be removed |
| * directly. |
| */ |
| public boolean removeMember(Principal member) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| |
| /** |
| * Removes members from the group. |
| * Performs an LDAP modify to remove the LDAP URL. |
| * |
| * @param members The LDAP URL describing the members to be removed. |
| * @return boolean true if the removal was successful; false otherwise. |
| * @throws NamingException The exception is thrown if an error occurs |
| * while performing LDAP modify. |
| * @throws IllegalStateException The exception is thrown if the |
| * group does not represent a group in the directory. |
| */ |
| public void removeMembers(String members) throws NamingException { |
| if (!isBound()) { |
| throw new IllegalStateException(); |
| } |
| if (debug) { |
| System.out.println("[debug] removing the members: " + members); |
| } |
| modifyMembers(members, DirContext.REMOVE_ATTRIBUTE); |
| } |
| |
| /** |
| * Retrieves the distinguished name of the group. |
| * |
| * @return String The distinguished name of the group. |
| * @throws IllegalStateException The exception is thrown if the |
| * group does not represent a group in the directory. |
| */ |
| public String getName() { |
| if (!isBound()) { |
| throw new IllegalStateException(); |
| } |
| return groupDN; |
| } |
| |
| /** |
| * Sets the distinguished name of the group. |
| * This method is called by {@link LdapGroupFactory} |
| * |
| * @param groupDN The group's distinguished name. |
| * @param ctx An LDAP context. |
| * @param name The group's name relative to the context. |
| */ |
| // package private (used by LdapGroupFactory) |
| void setName(String groupDN, DirContext ctx, Name name) { |
| bindDN = groupDN; |
| bindCtx = ctx; |
| bindName = name; |
| } |
| |
| /** |
| * Creates a string representation of the group. |
| * |
| * @return String A string listing the distinguished name of the group and |
| * the contents of the group's attribute set. See |
| * {@link javax.naming.directory.BasicAttributes#toString()} |
| * for details. The name is omitted if the group is not bound in |
| * the directory and empty string is returned if no attributes are |
| * available. |
| */ |
| public String toString() { |
| isBound(); // refresh attributes (if necessary) |
| if (groupDN != null) { |
| StringBuffer buffer = new StringBuffer(); |
| buffer.append("{name: ").append(groupDN).append("}"); |
| if (attributes != null) { |
| buffer.append(attributes.toString()); |
| } |
| return buffer.toString(); |
| } else { |
| return attributes == null ? "" : attributes.toString(); |
| } |
| } |
| |
| /** |
| * Retrieves the group's attributes. |
| * This method is called by {@link LdapGroupFactory}. |
| * |
| * @return Attribute The group's attributes. |
| */ |
| // package private (used by LdapGroupFactory) |
| Attributes getAttributes() { |
| return attributes; |
| } |
| |
| /** |
| * Determines whether the supplied LDAP objectClass attribute matches that |
| * of the group. A match occurs if the argument contains the value |
| * "GroupOfURLs". |
| * |
| * @param objectClass The non-null objectClass attribute to check against. |
| * @return true if the objectClass attributes match; false otherwise. |
| */ |
| // package private (used by LdapGroupFactory) |
| static boolean matches(Attribute objectClass) { |
| try { |
| for (Enumeration values = objectClass.getAll(); values.hasMoreElements();) { |
| if (OBJECT_CLASS.equalsIgnoreCase((String) values.nextElement())) { |
| return true; |
| } |
| } |
| } catch (NamingException e) { |
| if (debug) { |
| System.out.println("[debug] error matching objectClass: " + e); |
| } |
| // ignore |
| } |
| return false; |
| } |
| |
| /** |
| * Determines whether the group object is bound in the directory. |
| * |
| * A group object is considered to be bound in the directory when |
| * each of the values of its objectClass attribute match those of |
| * an object in the directory having this group's distinguished name. |
| * |
| * @return true if the object is bound; false otherwise. |
| */ |
| private boolean isBound() { |
| if (objectIsBound) { |
| return true; |
| |
| } else if (bindCtx != null && bindName != null && attributes != null) { |
| try { |
| // Retrieve the group's attributes |
| Attributes bindAttrs = bindCtx.getAttributes(bindName); |
| Attribute bindObjectClass = bindAttrs.get("objectClass"); |
| // Check whether the objectClass attributes match |
| if (bindObjectClass != null && bindObjectClass.equals(attributes.get("objectClass"))) { |
| // Set the group's bound state |
| initializeBoundState(bindDN, bindCtx, bindName, env, bindAttrs); |
| return true; |
| } |
| |
| } catch (NameNotFoundException e) { |
| if (debug) { |
| System.out.println("[debug] object is not bound: " + e); |
| } |
| // ignore |
| |
| } catch (NamingException e) { |
| if (debug) { |
| System.out.println("[debug] error checking if bound: " + e); |
| } |
| // ignore |
| } |
| // Reset state to unbound |
| bindDN = null; |
| bindCtx = null; |
| bindName = null; |
| } |
| return false; |
| } |
| |
| |
| /** |
| * Releases the naming context created by this group. |
| * Closes the root naming context if one had been created. |
| * |
| * @throws NamingException If a naming exception is encountered while |
| * closing the root context. |
| */ |
| public void close() throws NamingException { |
| if (rootCtx != null && rootCtx != ctx) { |
| rootCtx.close(); |
| rootCtx = null; |
| } |
| } |
| |
| |
| /** |
| * Initialize the group's state when bound. |
| */ |
| private void initializeBoundState(String groupDN, DirContext ctx, Name name, Hashtable env, Attributes attributes) { |
| objectIsBound = true; |
| this.groupDN = groupDN; |
| this.ctx = ctx; |
| this.name = name; |
| this.env = env; |
| if (env == null && ctx != null) { |
| try { |
| this.env = ctx.getEnvironment(); |
| } catch (NamingException e) { |
| // ignore |
| } |
| } |
| if (env != null) { |
| String expandGroup = (String) env.get(EXPAND_GROUP); |
| if ("false".equalsIgnoreCase(expandGroup)) { |
| this.expandGroup = false; |
| } |
| } |
| this.attributes = attributes; |
| memberAttr = attributes != null ? attributes.get(MEMBER_ATTR_ID) : null; |
| } |
| |
| |
| /** |
| * Add or remove a value from the memberURL attribute. |
| */ |
| private void modifyMembers(String members, int mod_op) throws NamingException { |
| Attribute memberURL = new BasicAttribute(MEMBER_ATTR_ID, members); |
| modification[0] = new ModificationItem(mod_op, memberURL); |
| ctx.modifyAttributes(name, modification); |
| memberAttr = null; // invalidate the cache |
| } |
| |
| |
| /** |
| * Checks if the supplied name is a member of any subgroups. |
| * Only subgroups are retrieved and explored. |
| */ |
| private boolean isSubgroupMember(String dn) throws NamingException { |
| for (NamingEnumeration members = (NamingEnumeration) members(GROUPS_ONLY); members.hasMore();) { |
| Object obj = members.next(); |
| if (obj instanceof GroupOfNames && ((GroupOfNames) obj).isMember(dn)) { |
| members.close(); // cleanup |
| return true; |
| } else if (obj instanceof GroupOfURLs && ((GroupOfURLs) obj).isMember(dn)) { |
| members.close(); // cleanup |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| |
| /** |
| * Search for the member using a group LDAP URL. |
| */ |
| private NamingEnumeration searchUsingLdapUrl(String memberUrl, SearchControls searchControls, String memberDn) |
| throws NamingException { |
| return searchUsingLdapUrl(memberDn, memberUrl, null, searchControls); |
| } |
| |
| |
| /** |
| * Search for the members using a group LDAP URL. |
| */ |
| private NamingEnumeration searchUsingLdapUrl(String memberUrl, String filter, SearchControls searchControls) |
| throws NamingException { |
| return searchUsingLdapUrl(null, memberUrl, filter, searchControls); |
| } |
| |
| |
| /** |
| * Search for the members using a group LDAP URL. |
| * When memberDn is not null then the URL filter is modified to include |
| * matching the values of its RDNs. |
| * When andFilter is not null then the URL filter is modified to "and" it |
| * with the supplied filter. |
| */ |
| private NamingEnumeration searchUsingLdapUrl(String memberDn, String memberUrl, String andFilter, |
| SearchControls searchControls) throws NamingException { |
| LdapURL url = new LdapURL(memberUrl); |
| |
| String filter = url.getFilter(); |
| Object[] filterArgs = null; |
| |
| // Modify the URL filter to include the member's DN |
| if (memberDn != null) { |
| ArrayList filterAndArgs = restrictFilter(filter, memberDn); |
| filter = (String) filterAndArgs.remove(filterAndArgs.size() - 1); |
| filterArgs = filterAndArgs.toArray(); |
| |
| // Modify the URL filter to include the supplied filter |
| } else if (andFilter != null) { |
| StringBuffer filterBuffer = new StringBuffer(filter.length() + andFilter.length() + 3); |
| filter = filterBuffer.append("(&").append(filter).append(andFilter).append(")").toString(); |
| } |
| |
| // Set the scope of the search |
| String scope = url.getScope(); |
| if ("sub".equalsIgnoreCase(scope)) { |
| searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); |
| } else if ("base".equalsIgnoreCase(scope)) { |
| searchControls.setSearchScope(SearchControls.OBJECT_SCOPE); |
| } else if ("one".equalsIgnoreCase(scope)) { |
| searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE); |
| } |
| |
| // Create the root context |
| if (rootCtx == null) { |
| // test for the root of the namespace (the empty name) |
| if (ctx.getNameInNamespace().length() == 0) { |
| rootCtx = ctx; // context is already a root context |
| } else { |
| // Make properties suitable for the root context |
| env = GroupOfNames.generateRootContextProperties(env); |
| rootCtx = (DirContext) NamingManager.getInitialContext(env); |
| // Propagate any context request controls (to rootCtx) |
| if (ctx instanceof LdapContext) { |
| ((LdapContext) rootCtx).setRequestControls(((LdapContext) ctx).getRequestControls()); |
| } |
| } |
| } |
| |
| // Perform the search from the root context |
| return memberDn != null |
| ? rootCtx.search(url.getDN(), filter, filterArgs, searchControls) |
| : rootCtx.search(url.getDN(), filter, searchControls); |
| } |
| |
| /** |
| * Limit the scope of a filter to include the supplied distinguished name. |
| * |
| * The filter generated by this method is guaranteed to match the entry |
| * that both satisfies the supplied filter and that has the supplied |
| * distinguished name. Note however that applying the generated filter |
| * may return additional entries. This is because while the components of |
| * a distinguished name are ordered the components of a filter are not. |
| * Thus the filter will match any distinguished name where all of its |
| * RDNs match the filter in any order. |
| * <p> |
| * The generated filter is returned as the last element of the returned |
| * ArrayList. It uses the filter expression syntax supported by |
| * {@link javax.naming.directory.DirContext#search(javax.naming.Name, |
| * java.lang.String, javax.naming.directory.SearchControls) DirContext.search} |
| * <p> |
| * For example, a filter such as "(objectClass=person)" and the |
| * distinguished name "cn=joe,o=abc,c=us" generates the new filter: |
| * "(&(objectClass=person)(cn={0})(o:dn:={1})(c:dn:={2}))" |
| * where the variables <code>{i}</code> are attribute values from the |
| * distinguished name and are returned in the filter arguments array. |
| * |
| * @param filter The filter to be modified. |
| * @param dn The distinguished name to be included in the filter. |
| * @return A list of attribute values extracted from the distinguished name. |
| * Its final element contains the generated filter string. |
| * @throws InvalidNameException If the distinguished name is invalid. |
| */ |
| private static ArrayList restrictFilter(String filter, String dn) throws InvalidNameException { |
| StringBuffer filterBuffer = new StringBuffer(filter.length() + 2 * dn.length()); |
| filterBuffer.append("(&").append(filter); |
| int filterIndex = 0; |
| ArrayList filterArgs = new ArrayList(); |
| LdapName name = new LdapName(dn); |
| int rdnCount = name.size(); |
| for (int i = rdnCount; i > 0; i--) { |
| String rdn = (String) name.get(i - 1); |
| |
| // handle multi-valued RDNs |
| int plus = 0; |
| int start = 0; |
| while ((plus = rdn.indexOf('+', start)) >= 0 && rdn.charAt(plus - 1) != '\\') { |
| filterArgs.add( |
| appendFilterComponent(filterBuffer, filterIndex++, rdn.substring(start, plus), i == rdnCount) |
| ); |
| start = plus + 1; |
| } |
| if (start == 0) { |
| rdn = rdn.substring(start); |
| } |
| filterArgs.add(appendFilterComponent(filterBuffer, filterIndex++, rdn, i == rdnCount)); |
| } |
| filterBuffer.append(")"); |
| |
| // Ugh. Stuff the generated filter into the last element of filterArgs. |
| filterArgs.add(filterBuffer.toString()); |
| return filterArgs; |
| } |
| |
| /** |
| * Generates a filter component from an RDN, appends it to the supplied |
| * filter and returns the RDN's attribute value. The filter component |
| * uses the filter expression syntax supported by |
| * {@link javax.naming.directory.DirContext#search(javax.naming.Name, |
| * java.lang.String, javax.naming.directory.SearchControls) DirContext.search} |
| * <p> |
| * For example, the RDN "cn=joe" produces the filter component "(cn={i})". |
| * |
| * @param filterBuffer The filter to append to. |
| * @param filterIndex An index into the filter arguments. |
| * @param rdn The RDN to examine. |
| * @param leastSignificantRdn A flag to indicate the least significant RDN. |
| * @return The attribute value in a String or byte array. |
| */ |
| private static Object appendFilterComponent(StringBuffer filterBuffer, int filterIndex, String rdn, |
| boolean leastSignificantRdn) { |
| // locate the separator |
| int equals = rdn.indexOf('='); |
| // extract the attribute ID |
| String attrID = rdn.substring(0, equals); |
| // extract the (unescaped) attribute value |
| Object attrValue = LdapName.unescapeAttributeValue(rdn.substring(equals + 1)); |
| // all but the least significant RDN activate DN-attributes matching |
| if (leastSignificantRdn) { |
| filterBuffer.append("(").append(attrID).append("={").append(filterIndex).append("})"); |
| } else { |
| filterBuffer.append("(").append(attrID).append(":dn:={").append(filterIndex).append("})"); |
| } |
| return attrValue; |
| } |
| |
| |
| /** |
| * The members of a dynamic group. |
| */ |
| class Members implements NamingEnumeration { |
| |
| private NamingEnumeration memberUrls = null; |
| private String filter = null; |
| private SearchControls searchControls = null; |
| private NamingEnumeration results = null; |
| private boolean expandSubgroups; |
| private ArrayList subgroups = null; |
| private NamingEnumeration subgroupMembers = null; |
| |
| /** |
| * Create an object for enumerating the members of a dynamic group. |
| */ |
| Members() { |
| if (debug) { |
| System.out.println("[debug] constructing an empty GroupOfURLs.Members object"); |
| } |
| expandSubgroups = expandGroup; // GroupOfURLs.expandGroup |
| } |
| |
| /** |
| * Create an object for enumerating the members of a dynamic group. |
| * |
| * @param memberUrls The group's LDAP URLs. |
| * @param filter A filter to apply to the group's members; it may be null. |
| * @param searchControls The search constraints used when retrieving group |
| * members. |
| */ |
| Members(NamingEnumeration memberUrls, String filter, SearchControls searchControls) { |
| if (debug) { |
| System.out.println("[debug] constructing a GroupOfURLs.Members object"); |
| } |
| this.memberUrls = memberUrls; |
| this.filter = filter; |
| this.searchControls = searchControls; |
| expandSubgroups = expandGroup; // GroupOfURLs.expandGroup |
| } |
| |
| /** |
| * Check if the group has more members. |
| * |
| * @return true if the group has another member. |
| */ |
| public boolean hasMoreElements() { |
| try { |
| return hasMore(); |
| } catch (NamingException e) { |
| if (debug) { |
| System.out.println("[debug] error checking for more members: " + |
| e); |
| } |
| return false; |
| } |
| } |
| |
| /** |
| * Check if the group has more members. |
| * |
| * @return true if the group has another member. |
| * @throws NamingException If a problem is encountered while checking |
| * whether the group has any more members. |
| */ |
| public boolean hasMore() throws NamingException { |
| if (memberUrls == null) { |
| return false; // empty |
| } |
| |
| // Get the first batch of members (if necessary) |
| if (results == null) { |
| results = searchUsingLdapUrl((String) memberUrls.next(), filter, searchControls); |
| } |
| |
| if (results.hasMore()) { |
| return true; |
| } else if (memberUrls.hasMore()) { |
| // Resolve the next member URL to get the next batch of members |
| results = searchUsingLdapUrl((String) memberUrls.next(), filter, searchControls); |
| if (hasMore()) { |
| return true; |
| } |
| } |
| |
| // Check subgroups |
| if (expandSubgroups && subgroups != null) { |
| if (subgroupMembers == null && (!subgroups.isEmpty())) { |
| // Retrieve the first subgroup's members |
| subgroupMembers = (NamingEnumeration) ((GroupPrincipal) subgroups.remove(0)).members(); |
| } |
| if (null != subgroupMembers && subgroupMembers.hasMore()) { |
| return true; |
| } else if (!subgroups.isEmpty()) { |
| // Retrieve the next subgroup's members |
| subgroupMembers = (NamingEnumeration) ((GroupPrincipal) subgroups.remove(0)).members(); |
| return subgroupMembers.hasMore(); |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Retrieve the next member of the group. |
| * Some members may themselves be groups. Such a member is returned as |
| * an object of class {@link com.sun.enterprise.security.GroupPrincipal}. |
| * |
| * @return The next member of the group. |
| * When only the {@link LdapGroupFactory} object factory is active |
| * then an object of class |
| * {@link java.security.Principal} or |
| * {@link com.sun.enterprise.security.GroupPrincipal} is returned. |
| * However, when additional object factories are active then an |
| * object of a different class may be returned. |
| * @throws NoSuchElementException If no more members exist or if a |
| * {@link javax.naming.NamingException} was encountered while |
| * retrieving the next element. |
| */ |
| public Object nextElement() { |
| try { |
| return next(); |
| } catch (NamingException e) { |
| // Exception.initCause is cleaner but depends on J2SE v 1.4 |
| throw new NoSuchElementException(e.toString()); |
| } |
| } |
| |
| /** |
| * Retrieve the next member of the group. |
| * Some members may themselves be groups. Such a member is returned as |
| * an object of class {@link com.sun.enterprise.security.GroupPrincipal}. |
| * |
| * @return The next member of the group. |
| * When only the {@link LdapGroupFactory} object factory is active |
| * then an object of class |
| * {@link java.security.Principal} or |
| * {@link com.sun.enterprise.security.GroupPrincipal} is returned. |
| * However, when additional object factories are active then an |
| * object of a different class may be returned. |
| * @throws NamingException If a problem is encountered while retrieving the |
| * next member of the group. |
| * @throws NoSuchElementException if no more members exist or if a |
| * {@link javax.naming.NamingException} was encountered while |
| * retrieving the next element. |
| */ |
| public Object next() throws NamingException { |
| |
| Object object; |
| String memberDN; |
| |
| if (memberUrls == null) { |
| throw new NoSuchElementException(); // empty |
| } |
| |
| // Get the first batch of members (if necessary) |
| if (results == null) { |
| results = searchUsingLdapUrl((String) memberUrls.next(), filter, searchControls); |
| } |
| |
| if (results.hasMore()) { |
| object = ((SearchResult) results.next()).getObject(); |
| |
| if (object instanceof GroupPrincipal) { |
| if (expandSubgroups) { |
| if (subgroups == null) { |
| subgroups = new ArrayList(); |
| } |
| subgroups.add(object); |
| } |
| // Subgroups are members too |
| return object; |
| |
| } else if (object instanceof DirContext) { |
| memberDN = ((DirContext)object).getNameInNamespace(); |
| ((DirContext)object).close(); // cleanup |
| return new LdapPrincipal(memberDN); |
| } else { |
| return object; // additional object factories are active |
| } |
| |
| } else if (memberUrls.hasMore()) { |
| // Resolve the next member URL to get the next batch of members |
| results = searchUsingLdapUrl((String) memberUrls.next(), filter, searchControls); |
| if (hasMore()) { |
| return next(); |
| } |
| } |
| |
| // Check subgroups |
| if (expandSubgroups && subgroups != null) { |
| if (subgroupMembers == null && (! subgroups.isEmpty())) { |
| // Retrieve the first subgroup's members |
| subgroupMembers = (NamingEnumeration)((GroupPrincipal)subgroups.remove(0)).members(); |
| } |
| if (null != subgroupMembers && subgroupMembers.hasMore()) { |
| return subgroupMembers.next(); |
| } else if (! subgroups.isEmpty()) { |
| // Retrieve the next subgroup's members |
| subgroupMembers = (NamingEnumeration)((GroupPrincipal)subgroups.remove(0)).members(); |
| return subgroupMembers.next(); |
| } |
| } |
| |
| throw new NoSuchElementException(); |
| } |
| |
| /** |
| * Closes the enumeration and releases its resources. |
| * |
| * @throws NamingException If a problem is encountered while closing the enumeration. |
| */ |
| public void close() throws NamingException { |
| if (results != null) { |
| results.close(); // cleanup |
| } |
| if (subgroupMembers != null) { |
| subgroupMembers.close(); // cleanup |
| } |
| } |
| } |
| } |