/**
* 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.testers;
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.IEntityGroup;
import org.apereo.portal.groups.IGroupMember;
import org.apereo.portal.groups.pags.IPersonTester;
import org.apereo.portal.groups.pags.dao.IPersonAttributesGroupTestDefinition;
import org.apereo.portal.security.IPerson;
import org.apereo.portal.services.GroupService;
import org.apereo.portal.spring.locator.ApplicationContextLocator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
/**
* Immutable PAGS Tester for inclusive/exclusive membership in sets of groups. To avoid infinite
* recursion, calls to {@code test()} are tracked by parameters plus thread ID.
*
* <p>{@code Attribute} should be either {@code group-member} or {@code not-group-member}.
*
* <p>{@code Value} should be the group name as expected by {@code GroupService.searchForGroups}.
* {@code GroupService} is searched every test since groups can be added without restarting uPortal.
*
* @see org.apereo.portal.groups.pags.IPersonTester
* @see org.apereo.portal.groups.pags.dao.EntityPersonAttributesGroupStore
* @see org.apereo.portal.groups.pags.dao.IPersonAttributesGroupTestGroupDefinition
* @see org.apereo.portal.services.GroupService
* @since 4.3
*/
public final class AdHocGroupTester implements IPersonTester {
public static final String MEMBER_OF = "group-member";
public static final String NOT_MEMBER_OF = "not-group-member";
private final Cache currentTests;
private final Logger logger = LoggerFactory.getLogger(getClass());
private final String groupName;
private final boolean isNotTest;
private final String groupHash;
public AdHocGroupTester(IPersonAttributesGroupTestDefinition definition) {
assert (definition.getAttributeName().equals(MEMBER_OF)
|| definition.getAttributeName().equals(NOT_MEMBER_OF));
this.isNotTest = definition.getAttributeName().equals(NOT_MEMBER_OF);
this.groupName = definition.getTestValue();
this.groupHash = calcGroupHash(groupName, isNotTest);
ApplicationContext context = ApplicationContextLocator.getApplicationContext();
CacheManager cacheManager = context.getBean("cacheManager", CacheManager.class);
this.currentTests =
cacheManager.getCache("org.apereo.portal.groups.pags.testers.AdHocGroupTester");
}
/*
* At some point, a person is being tested for group membership. During that test, the thread hits an ad hoc group
* tester. When that tester calls isDeepMemberOf, a test for group membership is triggered. During this call stack,
* the second call the the ad hoc group tester returns false. Assuming the group hierarchy is not itself recursive
* for the group containing the ad hoc group test, the test returns a usable value.
*
* If there is no caching and the second person object only exists for the recursive call, then the implementation
* works.
*
* Also, if the person object is cached and used twice, then the group key with the ad hoc tester is not added to
* the containing group keys during the recursion but is added (or not) after the test call returns positive.
*/
@Override
public boolean test(IPerson person) {
String personHash =
person.getEntityIdentifier().getKey() + groupHash + Thread.currentThread().getId();
logger.debug("Entering test() for {}", personHash);
IEntityGroup entityGroup = findGroupByName(groupName);
if (entityGroup == null) {
logger.error(
"Group named '{}' in ad hoc group tester definition not found!!", groupName);
return false;
}
IGroupMember gmPerson = findPersonAsGroupMember(person);
if (currentTests.getQuiet(personHash) != null) {
logger.debug(
"Returning from test() for {} due to recusion for person = {}",
personHash,
person.toString());
return false; // stop recursing
}
Element cacheEl = new Element(personHash, personHash);
currentTests.put(cacheEl);
// method that potentially recurs
boolean isPersonGroupMember = gmPerson.isDeepMemberOf(entityGroup);
currentTests.remove(personHash);
logger.debug("Returning from test() for {}", personHash);
return isPersonGroupMember ^ isNotTest;
}
/**
* Create a hash based on the group name and member-of/not-member-of test. This will be part of
* the call hash key used to detect recursive calls to the same test (although this may be a
* different instance).
*
* <p>Format for member-of test: _+{@code groupName}_# Format for not-member-of test: _^{@code
* groupName}_# Example for member-of Students: _+Students_#
*
* @param groupName group name to hash
* @param isNotTest whether the test is for not-member-of
* @return hash for this test based on group name and test type parameters
*/
private static String calcGroupHash(String groupName, boolean isNotTest) {
return (isNotTest ? "_^" : "_+") + groupName + "_#";
}
/**
* Find {@link IEntityGroup} from group name.
*
* @param groupName name of group to search from {@code GroupService}
* @return {@code IEntityGroup} with given name or null if no group with given name found
* @see org.apereo.portal.services.GroupService#searchForEntities(String, int, Class)
* @see org.apereo.portal.services.GroupService#findGroup(String)
*/
private static IEntityGroup findGroupByName(String groupName) {
EntityIdentifier[] identifiers =
GroupService.searchForGroups(groupName, GroupService.IS, IPerson.class);
for (EntityIdentifier entityIdentifier : identifiers) {
if (entityIdentifier.getType().equals(IEntityGroup.class)) {
return GroupService.findGroup(entityIdentifier.getKey());
}
}
return null;
}
/**
* Find {@link IPerson} as {@link IGroupMember}.
*
* @param person {@code IPerson} with entity identifier key to look up
* @return person as {@code IGroupMember}
* @see org.apereo.portal.services.GroupService#getEntity(String, Class)
*/
private static IGroupMember findPersonAsGroupMember(IPerson person) {
String personKey = person.getEntityIdentifier().getKey();
return GroupService.getEntity(personKey, IPerson.class);
}
}