blob: 5e558c40a51377fbe58745cfc0e8736c9bdb9559 [file] [log] [blame]
/*
* 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
}
}
}
}