/** * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright ownership. Apereo * 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 the * following location: * * <p>http://www.apache.org/licenses/LICENSE-2.0 * * <p>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.apereo.portal.groups.pags.dao; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import net.sf.ehcache.Cache; import net.sf.ehcache.CacheManager; import net.sf.ehcache.Element; import org.apereo.portal.EntityIdentifier; import org.apereo.portal.groups.EntityImpl; import org.apereo.portal.groups.EntityTestingGroupImpl; import org.apereo.portal.groups.GroupsException; import org.apereo.portal.groups.IEntity; import org.apereo.portal.groups.IEntityGroup; import org.apereo.portal.groups.IEntityGroupStore; import org.apereo.portal.groups.IEntitySearcher; import org.apereo.portal.groups.IEntityStore; import org.apereo.portal.groups.IGroupMember; import org.apereo.portal.groups.ILockableEntityGroup; import org.apereo.portal.groups.pags.IPersonTester; import org.apereo.portal.groups.pags.PagsGroup; import org.apereo.portal.groups.pags.TestGroup; import org.apereo.portal.security.IPerson; import org.apereo.portal.security.PersonFactory; import org.apereo.portal.security.provider.RestrictedPerson; import org.apereo.portal.spring.locator.ApplicationContextLocator; import org.apereo.portal.spring.locator.EntityTypesLocator; import org.apereo.portal.spring.locator.PersonAttributeDaoLocator; import org.jasig.services.persondir.IPersonAttributeDao; import org.jasig.services.persondir.IPersonAttributes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; /** * The Person Attributes Group Store uses attributes stored in the IPerson object to determine group * membership. It can use attributes from any data source supported by the PersonDirectory service. * * @since 4.1 */ public class EntityPersonAttributesGroupStore implements IEntityGroupStore, IEntityStore, IEntitySearcher { private final Logger logger = LoggerFactory.getLogger(getClass()); private static final Class<IPerson> IPERSON_CLASS = IPerson.class; private static final EntityIdentifier[] EMPTY_SEARCH_RESULTS = new EntityIdentifier[0]; private IPersonAttributesGroupDefinitionDao personAttributesGroupDefinitionDao; /** Caches IEntityGroup (EntityGroupImpl) instances */ private final Cache entityGroupCache; /** Caches PagsGroup instances */ private final Cache pagsGroupCache; /** * Caches the result of evaluating a single IGroupMember's (direct) membership in a PAGS group */ private final Cache membershipCache; public EntityPersonAttributesGroupStore() { super(); ApplicationContext applicationContext = ApplicationContextLocator.getApplicationContext(); this.personAttributesGroupDefinitionDao = applicationContext.getBean( "personAttributesGroupDefinitionDao", IPersonAttributesGroupDefinitionDao.class); CacheManager cacheManager = applicationContext.getBean("cacheManager", CacheManager.class); this.entityGroupCache = cacheManager.getCache( "org.apereo.portal.groups.pags.dao.EntityPersonAttributesGroupStore.entityGroup"); this.pagsGroupCache = cacheManager.getCache( "org.apereo.portal.groups.pags.dao.EntityPersonAttributesGroupStore.pagsGroup"); this.membershipCache = cacheManager.getCache( "org.apereo.portal.groups.pags.dao.EntityPersonAttributesGroupStore.membership"); } @Override public boolean contains(IEntityGroup group, IGroupMember member) { /* * This method has the potential to be called A LOT, especially if * there's a lot of portal data (portlets & groups). It's important * not to waste time on nonsensical checks. */ if (!IPERSON_CLASS.equals(member.getLeafType())) { // Maybe this call to contains() shouldn't even happen, since // group.getLeafType() is (presumably) IPerson.class. return false; } if (member.isGroup()) { // PAGS groups may only contain other PAGS groups (and people, of course) final IEntityGroup ieg = (IEntityGroup) member; if (!PagsService.SERVICE_NAME_PAGS.equals(ieg.getServiceName().toString())) { return false; } } final MembershipCacheKey cacheKey = new MembershipCacheKey( group.getEntityIdentifier(), member.getUnderlyingEntityIdentifier()); Element element = membershipCache.get(cacheKey); if (element == null) { logger.debug( "Checking if group {} contains member {}/{}", group.getName(), member.getKey(), member.getLeafType().getSimpleName()); boolean answer = false; // default final PagsGroup groupDef = convertEntityToGroupDef(group); if (member.isGroup()) { final String key = ((IEntityGroup) member).getLocalKey(); answer = groupDef.hasMember(key); } else { try { final IPersonAttributeDao pa = PersonAttributeDaoLocator.getPersonAttributeDao(); final IPersonAttributes personAttributes = pa.getPerson(member.getKey()); if (personAttributes != null) { final RestrictedPerson rp = PersonFactory.createRestrictedPerson(); rp.setAttributes(personAttributes.getAttributes()); answer = groupDef.contains(rp); } } catch (Exception ex) { logger.error( "Exception acquiring attributes for member " + member + " while checking if group " + group + " contains this member.", ex); return false; } } element = new Element(cacheKey, answer); membershipCache.put(element); } return (Boolean) element.getObjectValue(); } private PagsGroup convertEntityToGroupDef(IEntityGroup group) { IPersonAttributesGroupDefinition pagsGroup = getPagsGroupDefByName(group.getName()); return initGroupDef(pagsGroup); } private IEntityGroup convertPagsGroupToEntity(IPersonAttributesGroupDefinition group) { final String cacheKey = group.getName(); Element element = entityGroupCache.get(cacheKey); if (element == null) { final IEntityGroup entityGroup = new EntityTestingGroupImpl(group.getName(), IPERSON_CLASS); entityGroup.setName(group.getName()); entityGroup.setDescription(group.getDescription()); element = new Element(cacheKey, entityGroup); entityGroupCache.put(element); } return (IEntityGroup) element.getObjectValue(); } @Override public void delete(IEntityGroup group) throws GroupsException { throw new UnsupportedOperationException( "EntityPersonAttributesGroupStore: Method delete() not supported."); } @Override public IEntityGroup find(String name) throws GroupsException { Set<IPersonAttributesGroupDefinition> groups = personAttributesGroupDefinitionDao.getPersonAttributesGroupDefinitionByName(name); if (groups.size() == 0) { logger.error( "No PAGS group with name {} found. Check your PAGS group definitions for possible error" + " in member group name", name); return null; } IPersonAttributesGroupDefinition pagsGroup = groups.iterator().next(); return convertPagsGroupToEntity(pagsGroup); } @Override public Iterator<IEntityGroup> findParentGroups(IGroupMember member) throws GroupsException { /* * This method has the potential to be called A LOT, especially if * there's a lot of portal data (portlets & groups). It's important * not to waste time on nonsensical checks. */ if (!IPERSON_CLASS.equals(member.getLeafType())) { // This is going to happen; GaP code is not responsible for // knowing that PAGS only supports groups of IPerson (we are). return Collections.emptyIterator(); } logger.debug("finding containing groups for member key {}", member.getKey()); final Set<IEntityGroup> set = Collections.emptySet(); Iterator<IEntityGroup> rslt = set.iterator(); // default if (member.isGroup()) { // PAGS groups may only contain other PAGS groups (and people, of course) final IEntityGroup ieg = (IEntityGroup) member; if (PagsService.SERVICE_NAME_PAGS.equals(ieg.getServiceName().toString())) { rslt = findParentGroupsForGroup((IEntityGroup) member); } } else { rslt = findParentGroupsForEntity((IEntity) member); } return rslt; } private Iterator<IEntityGroup> findParentGroupsForGroup(IEntityGroup group) { logger.debug( "Finding containing groups for group {} (key {})", group.getName(), group.getKey()); Set<IEntityGroup> parents = getParentGroups(group.getName(), new HashSet<IEntityGroup>()); return parents.iterator(); } private Iterator<IEntityGroup> findParentGroupsForEntity(IEntity member) throws GroupsException { Set<IPersonAttributesGroupDefinition> pagsGroups = personAttributesGroupDefinitionDao.getPersonAttributesGroupDefinitions(); List<IEntityGroup> results = new ArrayList<IEntityGroup>(); for (IPersonAttributesGroupDefinition pagsGroup : pagsGroups) { IEntityGroup group = convertPagsGroupToEntity(pagsGroup); if (contains(group, member)) { results.add(group); } } return results.iterator(); } @Override public Iterator<IEntityGroup> findEntitiesForGroup(IEntityGroup group) throws GroupsException { // PAGS groups are synthetic; we don't support this behavior. return Collections.emptyIterator(); } @Override public ILockableEntityGroup findLockable(String key) throws GroupsException { throw new UnsupportedOperationException( "EntityPersonAttributesGroupStore: Method findLockable() not supported"); } @Override public String[] findMemberGroupKeys(IEntityGroup group) throws GroupsException { List<String> keys = new ArrayList<String>(); PagsGroup groupDef = convertEntityToGroupDef( group); // Will prevent wasting time on non-PAGS groups, if those calls even happen if (groupDef != null) { for (Iterator<String> i = groupDef.getMembers().iterator(); i.hasNext(); ) { keys.add(i.next()); } } return keys.toArray(new String[] {}); } @Override public Iterator<IEntityGroup> findMemberGroups(IEntityGroup group) throws GroupsException { /* * The GaP system prevents this method from being called with a nn-PAGS group. */ IPersonAttributesGroupDefinition pagsGroup = getPagsGroupDefByName(group.getName()); List<IEntityGroup> results = new ArrayList<IEntityGroup>(); for (IPersonAttributesGroupDefinition member : pagsGroup.getMembers()) { results.add(convertPagsGroupToEntity(member)); } return results.iterator(); } @Override public IEntityGroup newInstance(Class entityType) throws GroupsException { throw new UnsupportedOperationException( "EntityPersonAttributesGroupStore: Method newInstance() not supported"); } @Override public EntityIdentifier[] searchForGroups(String query, int method, Class leaftype) throws GroupsException { if (leaftype != IPERSON_CLASS) { return EMPTY_SEARCH_RESULTS; } Set<IPersonAttributesGroupDefinition> pagsGroups = personAttributesGroupDefinitionDao.getPersonAttributesGroupDefinitions(); List<EntityIdentifier> results = new ArrayList<EntityIdentifier>(); switch (method) { case IS: for (IPersonAttributesGroupDefinition pagsGroup : pagsGroups) { IEntityGroup g = convertPagsGroupToEntity(pagsGroup); if (g.getName().equalsIgnoreCase(query)) { results.add(g.getEntityIdentifier()); } } break; case STARTS_WITH: for (IPersonAttributesGroupDefinition pagsGroup : pagsGroups) { IEntityGroup g = convertPagsGroupToEntity(pagsGroup); if (g.getName().toUpperCase().startsWith(query.toUpperCase())) { results.add(g.getEntityIdentifier()); } } break; case ENDS_WITH: for (IPersonAttributesGroupDefinition pagsGroup : pagsGroups) { IEntityGroup g = convertPagsGroupToEntity(pagsGroup); if (g.getName().toUpperCase().endsWith(query.toUpperCase())) { results.add(g.getEntityIdentifier()); } } break; case CONTAINS: for (IPersonAttributesGroupDefinition pagsGroup : pagsGroups) { IEntityGroup g = convertPagsGroupToEntity(pagsGroup); if (g.getName().toUpperCase().indexOf(query.toUpperCase()) != -1) { results.add(g.getEntityIdentifier()); } } break; } return results.toArray(new EntityIdentifier[] {}); } @Override public void update(IEntityGroup group) throws GroupsException { throw new UnsupportedOperationException( "EntityPersonAttributesGroupStore: Method update() not supported."); } @Override public void updateMembers(IEntityGroup group) throws GroupsException { throw new UnsupportedOperationException( "EntityPersonAttributesGroupStore: Method updateMembers() not supported."); } @Override public IEntity newInstance(String key, Class type) throws GroupsException { /* * NOTE: It seems like something should be done to prevent emitting * nonsense entities; it's not clear what that would be. */ if (EntityTypesLocator.getEntityTypes().getEntityIDFromType(type) == null) { throw new GroupsException("Invalid entity type: " + type.getName()); } return new EntityImpl(key, type); } @Override public EntityIdentifier[] searchForEntities(String query, int method, Class type) throws GroupsException { return EMPTY_SEARCH_RESULTS; } private PagsGroup initGroupDef(IPersonAttributesGroupDefinition group) { Element element = this.pagsGroupCache.get(group.getName()); if (element != null) { return (PagsGroup) element.getObjectValue(); } PagsGroup groupDef = new PagsGroup(); groupDef.setKey(group.getName()); groupDef.setName(group.getName()); groupDef.setDescription(group.getDescription()); addMemberKeys(groupDef, group.getMembers()); Set<IPersonAttributesGroupTestGroupDefinition> testGroups = group.getTestGroups(); for (IPersonAttributesGroupTestGroupDefinition testGroup : testGroups) { TestGroup tg = new TestGroup(); Set<IPersonAttributesGroupTestDefinition> tests = testGroup.getTests(); for (IPersonAttributesGroupTestDefinition test : tests) { IPersonTester testerInst = initializeTester(test); if (testerInst == null) { /* * A tester was intended that we cannot now recreate. This * is a potentially dangerous situation, since tests in PAGS * are "or-ed" together; a functioning group with a missing * test would have a wider membership, not narrower. (And * remember -- permissions are tied to groups.) We need to * play it safe and keep this group out of the mix. */ return null; } tg.addTest(testerInst); } groupDef.addTestGroup(tg); } element = new Element(group.getName(), groupDef); this.pagsGroupCache.put(element); return groupDef; } private void addMemberKeys(PagsGroup groupDef, Set<IPersonAttributesGroupDefinition> members) { for (IPersonAttributesGroupDefinition member : members) { groupDef.addMember(member.getName()); } } private IPersonTester initializeTester(IPersonAttributesGroupTestDefinition test) { try { Class<?> testerClass = Class.forName(test.getTesterClassName()); Constructor<?> c = testerClass.getConstructor(IPersonAttributesGroupTestDefinition.class); Object o = c.newInstance(test); return (IPersonTester) o; } catch (Exception e) { logger.error("Error in initializing tester class: {}", test.getTesterClassName(), e); return null; } } private Set<IEntityGroup> getParentGroups(String name, Set<IEntityGroup> groups) throws GroupsException { logger.debug("Looking up containing groups for {}", name); IPersonAttributesGroupDefinition pagsGroup = getPagsGroupDefByName(name); Set<IPersonAttributesGroupDefinition> pagsParentGroups = personAttributesGroupDefinitionDao.getParentPersonAttributesGroupDefinitions( pagsGroup); for (IPersonAttributesGroupDefinition pagsParent : pagsParentGroups) { IEntityGroup parent = convertPagsGroupToEntity(pagsParent); if (!groups.contains(parent)) { groups.add(parent); getParentGroups(pagsParent.getName(), groups); } else { throw new RuntimeException( "Recursive grouping detected! for " + name + " and parent " + pagsParent.getName()); } } return groups; } /** * Retrieve an implementation of {@code IPersonAttributesGroupDefinition} with the given {@code * name} from the JPA DAO. There are two assumptions. First, that the DAO handles caching, so * caching is not implemented here. Second, that group names are unique. A warning will be * logged if more than one group is found with the same name. * * @param name group name used to search for group definition * @return {@code IPersonAttributesGroupDefinition} of named group or null * @see IPersonAttributesGroupDefinitionDao#getPersonAttributesGroupDefinitionByName(String) * @see IPersonAttributesGroupDefinition */ private IPersonAttributesGroupDefinition getPagsGroupDefByName(String name) { Set<IPersonAttributesGroupDefinition> pagsGroups = personAttributesGroupDefinitionDao.getPersonAttributesGroupDefinitionByName(name); if (pagsGroups.size() > 1) { logger.error("More than one PAGS group with name {} found.", name); } final IPersonAttributesGroupDefinition rslt = pagsGroups.isEmpty() ? null : pagsGroups.iterator().next(); return rslt; } }