/**
* Licensed to Jasig under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Jasig licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.jasig.portal.groups.smartldap;
import java.net.URL;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.danann.cernunnos.Task;
import org.danann.cernunnos.runtime.RuntimeRequestResponse;
import org.danann.cernunnos.runtime.ScriptRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.jasig.portal.EntityIdentifier;
import org.jasig.portal.groups.ComponentGroupServiceDescriptor;
import org.jasig.portal.groups.EntityTestingGroupImpl;
import org.jasig.portal.groups.GroupsException;
import org.jasig.portal.groups.IEntityGroup;
import org.jasig.portal.groups.IEntityGroupStore;
import org.jasig.portal.groups.IEntityGroupStoreFactory;
import org.jasig.portal.groups.IGroupConstants;
import org.jasig.portal.groups.IGroupMember;
import org.jasig.portal.groups.ILockableEntityGroup;
import org.jasig.portal.security.IPerson;
import org.jasig.portal.security.PersonFactory;
import org.jasig.portal.services.PersonDirectory;
public final class SmartLdapGroupStore implements IEntityGroupStore {
// Instance Members.
private ApplicationContext spring_context = null;
/**
* Period after which SmartLdap will drop and rebuild the groups tree. May
* be overridden in SmartLdapGroupStoreConfix.xml. A value of zero or less
* (negative) disables this feature.
*/
private long groupsTreeRefreshIntervalSeconds = 900; // default
/**
* Timestamp (milliseconds) of the last tree refresh.
*/
private volatile long lastTreeRefreshTime = 0;
private final ScriptRunner runner;
private final Task initTask;
private final Log log = LogFactory.getLog(getClass());
/*
* Indexed Collections.
*/
/**
* Single-object abstraction that contains all knowledge of SmartLdap groups:
* <ul>
* <li>Map of all groups keyed by 'key' (DN). Includes ROOT_GROUP.</li>
* <li>Map of all parent relationships keyed by the 'key' (DN) of the child;
* the values are lists of the 'keys' (DNs) of its parents.
* Includes ROOT_GROUP.</li>
* <li>Map of all child relationships keyed by the 'key' (DN) of the parent;
* the values are lists of the 'keys' (DNs) of its children.
* Includes ROOT_GROUP.</li>
* <li>Map of all 'keys' (DNs) of SmartLdap managed groups indexed by group
* name in upper case. Includes ROOT_GROUP.</li>
* </ul>
*/
private GroupsTree groupsTree;
/*
* Public API.
*/
public static final String UNSUPPORTED_MESSAGE =
"The SmartLdap implementation of JA-SIG Groups and Permissions (GaP) " +
"does not support this operation.";
public static final String ROOT_KEY = "SmartLdap ROOT";
public static final String ROOT_DESC = "A root group provided for the SmartLdapGroupStore.";
public static final IEntityGroup ROOT_GROUP = createRootGroup();
public boolean contains(IEntityGroup group, IGroupMember member) throws GroupsException {
log.warn("Unsupported method accessed: SmartLdapGroupStore.contains");
throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
}
public void delete(IEntityGroup group) throws GroupsException {
log.warn("Unsupported method accessed: SmartLdapGroupStore.delete");
throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
}
/**
* Returns an instance of the <code>IEntityGroup</code> from the data store.
* @return org.jasig.portal.groups.IEntityGroup
* @param key java.lang.String
*/
public IEntityGroup find(String key) throws GroupsException {
if (isTreeRefreshRequired()) {
refreshTree();
}
if (log.isDebugEnabled()) {
log.debug("Invoking find() for key: " + key);
}
// All of our groups (incl. ROOT_GROUP)
// are indexed in the 'groups' map by key...
return groupsTree.getGroups().get(key);
}
/**
* Returns an <code>Iterator</code> over the <code>Collection</code> of
* <code>IEntityGroups</code> that the <code>IGroupMember</code> belongs to.
* @return java.util.Iterator
* @param gm org.jasig.portal.groups.IEntityGroup
*/
public Iterator findContainingGroups(IGroupMember gm) throws GroupsException {
if (isTreeRefreshRequired()) {
refreshTree();
}
List<IEntityGroup> rslt = new LinkedList<IEntityGroup>();
if (gm.isGroup()) {
// Check the local indeces...
IEntityGroup group = (IEntityGroup) gm;
List<String> list = groupsTree.getParents().get(group.getLocalKey());
if (list != null) {
// should only reach this code if its a SmartLdap managed group...
for (String s : list) {
rslt.add(groupsTree.getGroups().get(s));
}
}
} else if (gm.isEntity() && gm.getEntityType().equals(ROOT_GROUP.getEntityType())) {
// Ask the individual...
EntityIdentifier ei = gm.getUnderlyingEntityIdentifier();
Map<String,List<Object>> seed = new HashMap<String,List<Object>>();
List<Object> seedValue = new LinkedList<Object>();
seedValue.add(ei.getKey());
seed.put(IPerson.USERNAME, seedValue);
Map<String,List<Object>> attr = PersonDirectory.getPersonAttributeDao().getMultivaluedUserAttributes(seed);
// avoid NPEs and unnecessary IPerson creation
if (attr != null && !attr.isEmpty()) {
IPerson p = PersonFactory.createPerson();
p.setAttributes(attr);
// Analyze its memberships...
String attrName = (String) spring_context.getBean("memberOfAttributeName");
Object groupKeys = p.getAttributeValues(attrName);
// IPerson returns null if no value is defined for this attribute...
if (groupKeys != null) {
List<String> list = new LinkedList<String>();
if (groupKeys instanceof String) {
list.add((String) groupKeys);
} else if (groupKeys instanceof Object[]) {
Object[] objs = (Object[]) groupKeys;
for (Object o : objs) {
list.add((String) o);
}
} else if (groupKeys instanceof List) {
List<?> objs = (List<?>) groupKeys;
for (Object o : objs) {
list.add((String) o);
}
}
for (String s : list) {
if (groupsTree.getGroups().containsKey(s)) {
rslt.add(groupsTree.getGroups().get(s));
}
}
}
}
}
return rslt.iterator();
}
/**
* Returns an <code>Iterator</code> over the <code>Collection</code> of
* <code>IEntities</code> that are members of this <code>IEntityGroup</code>.
* @return java.util.Iterator
* @param group org.jasig.portal.groups.IEntityGroup
*/
public Iterator findEntitiesForGroup(IEntityGroup group) throws GroupsException {
if (isTreeRefreshRequired()) {
refreshTree();
}
if (log.isDebugEnabled()) {
log.debug("Invoking findEntitiesForGroup() for group: " + group.getLocalKey());
}
// We only deal w/ group-group relationships here...
return findMemberGroups(group);
}
public ILockableEntityGroup findLockable(String key) throws GroupsException {
log.warn("Unsupported method accessed: SmartLdapGroupStore.findLockable");
throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
}
/**
* Returns a <code>String[]</code> containing the keys of <code>IEntityGroups</code>
* that are members of this <code>IEntityGroup</code>. In a composite group
* system, a group may contain a member group from a different service. This is
* called a foreign membership, and is only possible in an internally-managed
* service. A group store in such a service can return the key of a foreign member
* group, but not the group itself, which can only be returned by its local store.
*
* @return String[]
* @param group org.jasig.portal.groups.IEntityGroup
*/
public String[] findMemberGroupKeys(IEntityGroup group) throws GroupsException {
if (isTreeRefreshRequired()) {
refreshTree();
}
if (log.isDebugEnabled()) {
log.debug("Invoking findMemberGroupKeys() for group: " + group.getLocalKey());
}
List<String> rslt = new LinkedList<String>();
for (Iterator it=findMemberGroups(group); it.hasNext();) {
IEntityGroup g = (IEntityGroup) it.next();
// Return composite keys here...
rslt.add(g.getKey());
}
return rslt.toArray(new String[0]);
}
/**
* Returns an <code>Iterator</code> over the <code>Collection</code> of
* <code>IEntityGroups</code> that are members of this <code>IEntityGroup</code>.
* @return java.util.Iterator
* @param group org.jasig.portal.groups.IEntityGroup
*/
public Iterator findMemberGroups(IEntityGroup group) throws GroupsException {
if (isTreeRefreshRequired()) {
refreshTree();
}
if (log.isDebugEnabled()) {
log.debug("Invoking findMemberGroups() for group: " + group.getLocalKey());
}
List<IEntityGroup> rslt = new LinkedList<IEntityGroup>();
List<String> list = groupsTree.getChildren().get(group.getLocalKey());
if (list != null) {
// should only reach this code if its a SmartLdap managed group...
for (String s : list) {
rslt.add(groupsTree.getGroups().get(s));
}
}
return rslt.iterator();
}
public IEntityGroup newInstance(Class entityType) throws GroupsException {
log.warn("Unsupported method accessed: SmartLdapGroupStore.newInstance");
throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
}
public EntityIdentifier[] searchForGroups(String query, int method, Class leaftype) throws GroupsException {
if (isTreeRefreshRequired()) {
refreshTree();
}
if (log.isDebugEnabled()) {
log.debug("Invoking searchForGroups(): query=" + query + ", method="
+ method + ", leaftype=" + leaftype.getClass().getName());
}
// We only match the IPerson leaf type...
if (!leaftype.equals(ROOT_GROUP.getEntityType())) {
return new EntityIdentifier[0];
}
// We need to escape regex special characters that appear in the query string...
final String[][] specials = new String[][] {
/* backslash must come first! */
new String[] { "\\", "\\\\"},
new String[] { "[", "\\[" },
/* closing ']' isn't needed b/c it's a normal character w/o a preceding '[' */
new String[] { "{", "\\{" },
/* closing '}' isn't needed b/c it's a normal character w/o a preceding '{' */
new String[] { "^", "\\^" },
new String[] { "$", "\\$" },
new String[] { ".", "\\." },
new String[] { "|", "\\|" },
new String[] { "?", "\\?" },
new String[] { "*", "\\*" },
new String[] { "+", "\\+" },
new String[] { "(", "\\(" },
new String[] { ")", "\\)" }
};
for (String[] s : specials) {
query = query.replace(s[0], s[1]);
}
// Establish the regex pattern to match on...
String regex = null;
switch (method) {
case IGroupConstants.IS:
regex = query.toUpperCase();
break;
case IGroupConstants.STARTS_WITH:
regex = query.toUpperCase() + ".*";
break;
case IGroupConstants.ENDS_WITH:
regex = ".*" + query.toUpperCase();
break;
case IGroupConstants.CONTAINS:
regex = ".*" + query.toUpperCase() + ".*";
break;
default:
String msg = "Unsupported search method: " + method;
throw new GroupsException(msg);
}
List<EntityIdentifier> rslt = new LinkedList<EntityIdentifier>();
for (Map.Entry<String,List<String>> y : groupsTree.getKeysByUpperCaseName().entrySet()) {
if (y.getKey().matches(regex)) {
List<String> keys = y.getValue();
for (String k : keys) {
rslt.add(new EntityIdentifier(k, IEntityGroup.class));
}
}
}
return rslt.toArray(new EntityIdentifier[0]);
}
public void update(IEntityGroup group) throws GroupsException {
log.warn("Unsupported method accessed: SmartLdapGroupStore.update");
throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
}
public void updateMembers(IEntityGroup group) throws GroupsException {
log.warn("Unsupported method accessed: SmartLdapGroupStore.updateMembers");
throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
}
/*
* Implementation.
*/
private static IEntityGroup createRootGroup() {
IEntityGroup rslt = new EntityTestingGroupImpl(ROOT_KEY, IPerson.class);
rslt.setCreatorID("System");
rslt.setName(ROOT_KEY);
rslt.setDescription(ROOT_DESC);
return rslt;
}
private SmartLdapGroupStore() {
// Spring tech...
URL u = getClass().getResource("/properties/groups/SmartLdapGroupStoreConfig.xml");
spring_context = new FileSystemXmlApplicationContext(u.toExternalForm());
// Interval between tree rebuilds
if (spring_context.containsBean("groupsTreeRefreshIntervalSeconds")) {
groupsTreeRefreshIntervalSeconds = (Long) spring_context.getBean("groupsTreeRefreshIntervalSeconds");
}
// Cernunnos tech...
runner = new ScriptRunner();
initTask = runner.compileTask(getClass().getResource("init.crn").toExternalForm());
}
private boolean isTreeRefreshRequired() {
if (groupsTree == null) {
// Of course we need it
return true;
}
if (groupsTreeRefreshIntervalSeconds <= 0) {
// SmartLdap refresh feature may be disabled by setting
// groupsTreeRefreshIntervalSeconds to zero or negative.
return false;
}
// The 'lastTreeRefreshTime' member variable is volatile. As of JDK 5,
// this fact should make reads of this variable dependable in a multi-
// threaded environment.
final long treeExpiresTimestamp = lastTreeRefreshTime + (groupsTreeRefreshIntervalSeconds * 1000L);
return System.currentTimeMillis() > treeExpiresTimestamp;
}
/**
* Verifies that the collection of groups needs rebuilding and, if so,
* spawns a new worker <code>Thread</code> for that purpose.
*/
private synchronized void refreshTree() {
if (!isTreeRefreshRequired()) {
// The groupsTree was already re-built while
// we were waiting to enter this method.
return;
}
// We must join the builder thread if
// we don't have an existing groupsTree.
final boolean doJoin = groupsTree == null;
// In most cases, re-build the tree in a separate thread; the current
// request can proceed with the newly-expired groupsTree.
Thread refresh = new Thread("SmartLdap Refresh Worker") {
public void run() {
// Replace the old with the new...
groupsTree = buildGroupsTree();
}
};
refresh.setDaemon(true);
refresh.start();
if (doJoin) {
try {
log.info("Joining the SmartLdap Refresh Worker Thread");
refresh.join();
} catch (InterruptedException ie) {
throw new RuntimeException(ie);
}
}
// Even if the refresh thread failed, don't try
// again for another groupsTreeRefreshIntervalSeconds.
lastTreeRefreshTime = System.currentTimeMillis();
}
private GroupsTree buildGroupsTree() {
// Prepare the new local indeces...
Map<String,IEntityGroup> new_groups = Collections.synchronizedMap(new HashMap<String,IEntityGroup>());
Map<String,List<String>> new_parents = Collections.synchronizedMap(new HashMap<String,List<String>>());
Map<String,List<String>> new_children = Collections.synchronizedMap(new HashMap<String,List<String>>());
Map<String,List<String>> new_keysByUpperCaseName = Collections.synchronizedMap(new HashMap<String,List<String>>());
// Gather IEntityGroup objects from LDAP...
RuntimeRequestResponse req = new RuntimeRequestResponse();
Set<LdapRecord> set = new HashSet<LdapRecord>();
req.setAttribute("GROUPS", set);
for (String name : spring_context.getBeanDefinitionNames()) {
req.setAttribute(name, spring_context.getBean(name));
}
runner.run(initTask, req);
if (log.isInfoEnabled()) {
String msg = "init() found " + set.size() + " records.";
log.info(msg);
}
// Do a first loop to build the main catalog (new_groups)...
for (LdapRecord r : set) {
// new_groups (me)...
IEntityGroup g = r.getGroup();
new_groups.put(g.getLocalKey(), g);
}
// Do a second loop to build local indeces...
for (LdapRecord r : set) {
IEntityGroup g = r.getGroup();
// new_parents (I am a parent for all my children)...
for (String childKey : r.getKeysOfChildren()) {
// NB: We're only interested in relationships between
// objects in the main catalog (i.e. new_groups);
// discard everything else...
if (!new_groups.containsKey(childKey)) {
break;
}
List<String> parentsList = new_parents.get(childKey);
if (parentsList == null) {
// first parent for this child...
parentsList = Collections.synchronizedList(new LinkedList<String>());
new_parents.put(childKey, parentsList);
}
parentsList.add(g.getLocalKey());
}
// new_children...
List<String> childrenList = Collections.synchronizedList(new LinkedList<String>());
for (String childKey : r.getKeysOfChildren()) {
// NB: We're only interested in relationships between
// objects in the main catalog (i.e. new_groups);
// discard everything else...
if (new_groups.containsKey(childKey)) {
childrenList.add(childKey);
}
}
new_children.put(g.getLocalKey(), childrenList);
// new_keysByUpperCaseName...
List<String> groupsWithMyName = new_keysByUpperCaseName.get(g.getName().toUpperCase());
if (groupsWithMyName == null) {
// I am the first group with my name (pretty likely)...
groupsWithMyName = Collections.synchronizedList(new LinkedList<String>());
new_keysByUpperCaseName.put(g.getName().toUpperCase(), groupsWithMyName);
}
groupsWithMyName.add(g.getLocalKey());
}
/*
* Now load the ROOT_GROUP into the collections...
*/
// new_groups (me)...
new_groups.put(ROOT_GROUP.getLocalKey(), ROOT_GROUP);
// new_parents (I am a parent for all groups that have no other parent)...
List<String> childrenOfRoot = Collections.synchronizedList(new LinkedList<String>()); // for later...
for (String possibleChildKey : new_groups.keySet()) {
if (!possibleChildKey.equals(ROOT_GROUP.getLocalKey()) && !new_parents.containsKey(possibleChildKey)) {
List<String> p = Collections.synchronizedList(new LinkedList<String>());
p.add(ROOT_GROUP.getLocalKey());
new_parents.put(possibleChildKey, p);
childrenOfRoot.add(possibleChildKey); // for later...
}
}
// new_children...
new_children.put(ROOT_GROUP.getLocalKey(), childrenOfRoot);
// new_keysByUpperCaseName...
List<String> groupsWithMyName = new_keysByUpperCaseName.get(ROOT_GROUP.getName().toUpperCase());
if (groupsWithMyName == null) {
// I am the first group with my name (pretty likely)...
groupsWithMyName = Collections.synchronizedList(new LinkedList<String>());
new_keysByUpperCaseName.put(ROOT_GROUP.getName().toUpperCase(), groupsWithMyName);
}
groupsWithMyName.add(ROOT_GROUP.getLocalKey());
if (log.isInfoEnabled()) {
String msg = "init() :: final size of each collection is as follows..."
+ "\n\tgroups=" + new_groups.size()
+ "\n\tparents=" + new_parents.size()
+ "\n\tchildren=" + new_children.size()
+ "\n\tkeysByUpperCaseName=" + new_keysByUpperCaseName.size();
log.info(msg);
}
if (log.isTraceEnabled()) {
StringBuilder msg = new StringBuilder();
// new_groups...
msg.setLength(0);
msg.append("Here are the keys of the new_groups collection:");
for (String s : new_groups.keySet()) {
msg.append("\n\t").append(s);
}
log.trace(msg.toString());
// new_parents...
msg.setLength(0);
msg.append("Here are the parents of each child in the new_parents collection:");
for (Map.Entry<String,List<String>> y : new_parents.entrySet()) {
msg.append("\n\tchild=").append(y.getKey());
for (String s : y.getValue()) {
msg.append("\n\t\tparent=").append(s);
}
}
log.trace(msg.toString());
// new_children...
msg.setLength(0);
msg.append("Here are the children of each parent in the new_children collection:");
for (Map.Entry<String,List<String>> y : new_children.entrySet()) {
msg.append("\n\tparent=").append(y.getKey());
for (String s : y.getValue()) {
msg.append("\n\t\tchild=").append(s);
}
}
log.trace(msg.toString());
// new_keysByUpperCaseName...
msg.append("Here are the groups that have each name in the new_keysByUpperCaseName collection:");
for (Map.Entry<String,List<String>> y : new_keysByUpperCaseName.entrySet()) {
msg.append("\n\tname=").append(y.getKey());
for (String s : y.getValue()) {
msg.append("\n\t\tgroup=").append(s);
}
}
log.trace(msg.toString());
}
return new GroupsTree(new_groups, new_parents, new_children, new_keysByUpperCaseName);
}
/*
* Nested Types.
*/
public static final class Factory implements IEntityGroupStoreFactory {
private static final IEntityGroupStore INSTANCE = new SmartLdapGroupStore();
/*
* Public API.
*/
public IEntityGroupStore newGroupStore() throws GroupsException {
return INSTANCE;
}
public IEntityGroupStore newGroupStore(ComponentGroupServiceDescriptor svcDescriptor) throws GroupsException {
return INSTANCE;
}
}
private static final class GroupsTree {
// Instance Members.
private final Map<String,IEntityGroup> groups;
private final Map<String,List<String>> parents;
private final Map<String,List<String>> children;
private final Map<String,List<String>> keysByUpperCaseName;
/*
* Public API.
*/
public GroupsTree(Map<String,IEntityGroup> groups, Map<String,List<String>> parents,
Map<String,List<String>> children,
Map<String,List<String>> keysByUpperCaseName) {
// Assertions.
if (groups == null) {
String msg = "Argument 'groups' cannot be null.";
throw new IllegalArgumentException(msg);
}
if (parents == null) {
String msg = "Argument 'parents' cannot be null.";
throw new IllegalArgumentException(msg);
}
if (children == null) {
String msg = "Argument 'children' cannot be null.";
throw new IllegalArgumentException(msg);
}
if (keysByUpperCaseName == null) {
String msg = "Argument 'keysByUpperCaseName' cannot be null.";
throw new IllegalArgumentException(msg);
}
// Instance Members.
this.groups = groups;
this.parents = parents;
this.children = children;
this.keysByUpperCaseName = keysByUpperCaseName;
}
public Map<String,IEntityGroup> getGroups() {
return groups;
}
public Map<String,List<String>> getParents() {
return parents;
}
public Map<String,List<String>> getChildren() {
return children;
}
public Map<String,List<String>> getKeysByUpperCaseName() {
return keysByUpperCaseName;
}
}
}