blob: af83092c951b9e13c04f3d8dacc72d972ccd7576 [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.LdapURL;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.naming.CompositeName;
import javax.naming.Context;
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.ldap.LdapContext;
import javax.naming.spi.NamingManager;
/**
* A representation of the LDAP groupOfNames object class.
* This is a static group: its members are listed in the group's member
* LDAP attribute.
* <p>
* Note that when a <tt>GroupOfNames</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>GroupOfNames</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>GroupOfNames</tt> instance is not synchronized against concurrent
* multithreaded access. Multiple threads trying to access and modify a
* <tt>GroupOfNames</tt> should lock the object.
* <p>
* In order to bind a <tt>GroupOfNames</tt> object in the directory, the
* following LDAP object class definition (RFC 2256) must be supported in the
* directory schema:
* <pre>
* ( 2.5.6.9 NAME 'groupOfNames'
* SUP top
* STRUCTURAL
* MUST ( member $
* cn )
* MAY ( businessCategory $
* seeAlso $
* owner $
* ou $
* o $
* description ) )
* </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 members
* Set members = new HashSet();
* members.add("cn=bill,ou=people");
* members.add("cn=ben,ou=people");
* members.add("cn=sysadmins,ou=groups");
* Group administrators = new GroupOfNames(members);
*
* // bind the group in the directory
* ctx.bind("cn=administrators,ou=groups", administrators);
*
* // list all of the group's members
* listMembers(administrators);
* ...
*
*
* // 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 GroupOfNames implements GroupPrincipal {
private static final boolean debug = false;
private static final String OBJECT_CLASS = "groupOfNames";
private static final String MEMBER_ATTR_ID = "member";
private static final String MEMBER_FILTER_EXPR = "(member={0})";
private static final String EXPAND_GROUP = "com.sun.jndi.ldap.obj.expandGroup";
private static final Attribute OBJECT_CLASS_ATTR = new BasicAttribute("objectClass", "top");
static {
OBJECT_CLASS_ATTR.add(OBJECT_CLASS);
}
private static final SearchControls BASE_SEARCH_NO_ATTRS = new SearchControls();
static {
BASE_SEARCH_NO_ATTRS.setSearchScope(SearchControls.OBJECT_SCOPE);
BASE_SEARCH_NO_ATTRS.setReturningAttributes(new String[0]); // no attrs
}
private boolean objectIsBound;
private boolean expandGroup = true;
private Attributes attributes = null;
private Attribute memberAttr = null;
private String memberAttrId;
private String memberFilterExpr;
private final Object[] filterArgs = new Object[1];
private final 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 GroupOfNames() {
if (debug) {
System.out.println("[debug] constructing an empty group");
}
initializeState(OBJECT_CLASS_ATTR, MEMBER_ATTR_ID, MEMBER_FILTER_EXPR, null);
}
/**
* Create a group object with an initial set of members.
* <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 members The set of initial members. It may be null.
* Each element is of class {@link String} or
* {@link java.security.Principal}.
*/
public GroupOfNames(Set members) {
if (debug) {
System.out.println("[debug] constructing a group");
}
initializeState(OBJECT_CLASS_ATTR, MEMBER_ATTR_ID, MEMBER_FILTER_EXPR, members);
}
/**
* Create a group object.
* This method is called by {@link GroupOfUniqueNames}.
*
* @param objectClass The LDAP objectClass attribute.
* @param memberAttrId The LDAP attribute ID which identifies the members.
* @param memberfilterExpr The filter expression used to find a member.
* @param members The set of initial members. It may be null.
*/
// package private (used by GroupOfUniqueNames)
GroupOfNames(Attribute objectClass, String memberAttrId, String memberFilterExpr, Set members) {
initializeState(objectClass, memberAttrId, memberFilterExpr, members);
}
/**
* Create a group object from its entry in the directory.
* This method is called by {@link GroupOfUniqueNames}.
*
* @param objectClass The LDAP objectClass attribute.
* @param memberAttrId The LDAP attribute ID which identifies the members.
* @param memberfilterExpr The filter expression used to find a member.
* @param members The set of initial members. It may be null.
* @param groupDN The group's distinguished name.
* @param name The group's LDAP 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.
*/
// package private (used by GroupOfUniqueNames)
GroupOfNames(Attribute objectClass, String memberAttrId,
String memberFilterExpr, Set members, String groupDN,
DirContext ctx, Name name, Hashtable env, Attributes attributes) {
initializeState(objectClass, memberAttrId, memberFilterExpr, members);
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 GroupOfNames(OBJECT_CLASS_ATTR, MEMBER_ATTR_ID,
MEMBER_FILTER_EXPR, null, groupDN, ctx, name, env, attributes);
}
/**
* Adds a member to the group.
* Performs an LDAP modify to add the member.
*
* @param member The name of the member to be added.
* @return boolean true if the addition was successful; false otherwise.
* @throws IllegalStateException The exception is thrown if the
* group does not represent a group in the directory.
*/
public boolean addMember(Principal member) {
try {
return addMember(member.getName());
} catch (NamingException e) {
if (debug) {
System.out.println("[debug] error adding the member: " + e);
}
return false;
}
}
/**
* Adds a member to the group.
* Performs an LDAP modify to add the member.
*
* @param dn The distinguished name (RFC 2253) of the member to be added.
* @return boolean true if the addition 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 boolean addMember(String dn) throws NamingException {
if (! isBound()) {
throw new IllegalStateException();
}
if (debug) {
System.out.println("[debug] adding the member: " + dn);
}
return modifyMember(dn, DirContext.ADD_ATTRIBUTE);
}
/**
* Checks if the supplied name is a member of the group.
* Performs LDAP searches 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.
*/
@Override
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 LDAP searches 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");
}
// Check cache
if (memberAttr != null && memberAttr.contains(dn)) {
return true;
}
// Check directory group
filterArgs[0] = dn;
NamingEnumeration results = ctx.search(name, memberFilterExpr, filterArgs, BASE_SEARCH_NO_ATTRS);
// Membership is confirmed if any results are returned
if (results != null && results.hasMore()) {
results.close(); // cleanup
return true;
}
// Check directory 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.
*/
@Override
public Enumeration members() {
if (!isBound()) {
throw new IllegalStateException();
}
if (debug) {
System.out.println("[debug] enumerating the members");
}
try {
// Retrieve the group's member attribute unless already cached
if ((memberAttr != null)
|| (memberAttr = ctx.getAttributes(name, new String[] {memberAttrId}).get(memberAttrId)) != null) {
return new Members(memberAttr.getAll());
}
} catch (NamingException e) {
if (debug) {
System.out.println("[debug] error enumerating the members: " + e);
}
// ignore
}
return new Members(); // empty
}
/**
* Removes a member from the group.
* Performs an LDAP modify to remove the member.
*
* @param member The name of the member to be removed.
* @return boolean true if the removal was successful; false otherwise.
* @throws IllegalStateException The exception is thrown if the
* group does not represent a group in the directory.
*/
public boolean removeMember(Principal member) {
try {
return removeMember(member.getName());
} catch (NamingException e) {
if (debug) {
System.out.println("[debug] error removing the member: " + e);
}
return false;
}
}
/**
* Removes a member from the group.
* Performs an LDAP modify to remove the member.
*
* @param dn The distinguished name (RFC 2253) of the member 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 boolean removeMember(String dn) throws NamingException {
if (!isBound()) {
throw new IllegalStateException();
}
if (debug) {
System.out.println("[debug] removing the member: " + dn);
}
return modifyMember(dn, 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.
*/
@Override
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 null is returned if no attributes are
* available.
*/
@Override
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
* "GroupOfNames".
*
* @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 The exception is thrown if a problem is
* encountered while closing the naming context.
*/
public void close() throws NamingException {
if (rootCtx != null && rootCtx != ctx) {
rootCtx.close();
rootCtx = null;
}
}
/**
* Initialize the group's state when unbound.
*/
private void initializeState(Attribute objectClass, String memberAttrId, String memberFilterExpr, Set members) {
objectIsBound = false;
this.memberAttrId = memberAttrId;
this.memberFilterExpr = memberFilterExpr;
// initialize the group's attribute set.
attributes = new BasicAttributes(true);
attributes.put(objectClass);
if (members != null && (!members.isEmpty())) {
memberAttr = new BasicAttribute(memberAttrId);
for (Object object : members) {
if (object instanceof Principal) {
memberAttr.add(((Principal) object).getName());
} else {
memberAttr.add(object);
}
}
attributes.put(memberAttr);
}
}
/**
* 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;
}
}
if (attributes != null) {
this.attributes = attributes;
memberAttr = attributes.get(memberAttrId);
}
}
/**
* Add or remove a value from the member attribute.
*/
private boolean modifyMember(String member, int mod_op) throws NamingException {
Attribute memberAttr = new BasicAttribute(memberAttrId, member);
modification[0] = new ModificationItem(mod_op, memberAttr);
ctx.modifyAttributes(name, modification);
this.memberAttr = null; // invalidate the cache
return true;
}
/**
* Checks if the supplied name is a member of any subgroups.
* All the members are retrieved and any subgroups are explored.
*/
private boolean isSubgroupMember(String dn) throws NamingException {
for (NamingEnumeration members = (NamingEnumeration) members(); 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;
}
/**
* Generate environment properties suitable for the root context using
* the supplied set of properties. The following properties are modified
* if necessary:
* <ul>
* <li> java.naming.provider.url property:
* trim any components after hostname/port.
* (identifies the root context).
* </ul>
*
* @param env A set of environment properties. It is cloned only when a
* modification is necessary.
* @return A set of environment properties suitable for the root context.
*/
// package private (used by GroupOfURLs)
static Hashtable generateRootContextProperties(Hashtable env) {
String url = null;
if (env != null) {
if ((url = (String) env.get(Context.PROVIDER_URL)) != null) {
try {
// java.net.URI is cleaner but depends on J2SE v 1.4
LdapURL ldapUrl = new LdapURL(url);
String dn = ldapUrl.getDN();
// check if a non-empty DN is present
if (dn != null && dn.length() > 0) {
String host = ldapUrl.getHost();
int port = ldapUrl.getPort();
url = "ldap://" + ((host != null) ? host : "") + ((port != -1) ? (":" + port) : "");
} else {
url = null; // reset flag
}
} catch (NamingException e) {
throw new IllegalArgumentException(url);
}
}
// only clone if making mods
if (url != null) {
env = (Hashtable) env.clone();
env.put(Context.PROVIDER_URL, url);
}
}
return env;
}
/**
* The members of a static group.
*/
class Members implements NamingEnumeration {
private NamingEnumeration memberDNs = null;
private final boolean expandSubgroups;
private ArrayList subgroups = null;
private NamingEnumeration subgroupMembers = null;
/**
* Empty members object derived from a static group.
*/
Members() {
if (debug) {
System.out.println("[debug] constructing an empty GroupOfNames.Members object");
}
expandSubgroups = expandGroup; // GroupOfNames.expandGroup
}
/**
* Members object derived from a static group.
*/
Members(NamingEnumeration memberDNs) {
if (debug) {
System.out.println("[debug] constructing a GroupOfNames.Members object");
}
this.memberDNs = memberDNs;
expandSubgroups = expandGroup; // GroupOfNames.expandGroup
}
/**
* Check if the group has more members.
*
* @return true if the group has another member.
*/
@Override
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.
*/
@Override
public boolean hasMore() throws NamingException {
if (memberDNs == null) {
return false; // empty
}
if (memberDNs.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}.
* <p>
* Note that in order to determine whether a member is itself a group
* this method reads each member's LDAP entry. As this 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 then an object of class {@link java.security.Principal}
* is returned. By default, the feature is enabled.
*
* @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.
*/
@Override
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}.
* <p>
* Note that in order to determine whether a member is itself a group
* this method reads each member's LDAP entry. As this 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 then an object of class {@link java.security.Principal}
* is returned. By default, the feature is enabled.
*
* @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.
*/
@Override
public Object next() throws NamingException {
if (memberDNs == null) {
throw new NoSuchElementException(); // empty
}
String memberDN = null;
try {
if (memberDNs.hasMore()) {
memberDN = (String)memberDNs.next();
// Skip lookup when expandGroup=false
if (! expandGroup) {
return new LdapPrincipal(memberDN);
}
// Create the root context
if (rootCtx == null) {
rootCtx = getRootContext();
}
// Perform the lookup from the root context
Object object = rootCtx.lookup(new CompositeName().add(memberDN));
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) {
((DirContext)object).close(); // cleanup
return new LdapPrincipal(memberDN);
} else {
return object; // additional object factories are active
}
}
} catch (NameNotFoundException e) {
// Cannot find the member's LDAP entry so return an LdapPrincipal
return new LdapPrincipal(memberDN);
}
// 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.
*/
@Override
public void close() throws NamingException {
if (subgroupMembers != null) {
subgroupMembers.close(); // cleanup
}
}
private DirContext getRootContext() throws NamingException {
DirContext 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 = generateRootContextProperties(env);
rootCtx = (DirContext) NamingManager.getInitialContext(env);
// Propagate any context request controls (to rootCtx)
if (ctx instanceof LdapContext) {
((LdapContext) rootCtx).setRequestControls(((LdapContext) ctx).getRequestControls());
}
}
return rootCtx;
}
}
}