Commit fe03f630 authored by Misagh Moayyed's avatar Misagh Moayyed
Browse files

Merge pull request #1157 from Unicon/cache-guava

Use Guava's cache for Principal attribute caching
parents 40ac919b acdf77d4
......@@ -511,7 +511,6 @@
<i class="fa fa-lg fa-question-circle form-tooltip-icon" data-toggle="tooltip" data-placement="top" title="<spring:message code="services.form.tooltip.attrRelease.principleAttRepo.cached.expiration" />"></i>
</label>
<div class="col-sm-8">
<%-- TODO: Shouldn't this be at least greater than 0? --%>
<input type="number" min="0" class="form-control" id="cachedExp" ng-model="serviceFormCtrl.serviceData.attrRelease.cachedExpiration" />
</div>
</div>
......
......@@ -201,16 +201,6 @@
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
<dependency>
<groupId>org.jsr107.ri</groupId>
<artifactId>cache-ri-impl</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
......
......@@ -21,7 +21,9 @@ package org.jasig.cas.authentication.principal;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.jasig.cas.authentication.principal.cache.AbstractPrincipalAttributesRepository;
import java.io.IOException;
import java.util.Map;
/**
......@@ -30,14 +32,23 @@ import java.util.Map;
* @author Misagh Moayyed
* @since 4.1
*/
public final class DefaultPrincipalAttributesRepository implements PrincipalAttributesRepository {
public final class DefaultPrincipalAttributesRepository extends AbstractPrincipalAttributesRepository {
private static final long serialVersionUID = -4535358847021241725L;
@Override
public Map<String, Object> getAttributes(final Principal p) {
protected void addPrincipalAttributes(final String id, final Map<String, Object> attributes) {
logger.debug("Using {}, no caching takes place for {} to add attributes.", id,
this.getClass().getSimpleName());
}
@Override
protected Map<String, Object> getPrincipalAttributes(final Principal p) {
logger.debug("{} will return the collection of attributes directly associated with the principal object",
this.getClass().getSimpleName());
return p.getAttributes();
}
@Override
public String toString() {
return new ToStringBuilder(this)
......@@ -65,4 +76,7 @@ public final class DefaultPrincipalAttributesRepository implements PrincipalAttr
return new HashCodeBuilder(13, 133)
.toHashCode();
}
@Override
public void close() throws IOException {}
}
/*
* 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:
*
* 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.cas.authentication.principal.cache;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import org.jasig.cas.authentication.principal.Principal;
import org.jasig.services.persondir.IPersonAttributeDao;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
/**
* Wrapper around an attribute repository where attributes cached for a configurable period
* based on google guava's caching library.
* @author Misagh Moayyed
* @since 4.2
*/
public final class CachingPrincipalAttributesRepository extends AbstractPrincipalAttributesRepository {
private static final long serialVersionUID = 6350244643948535906L;
private static final long DEFAULT_MAXIMUM_CACHE_SIZE = 1000;
private final transient Cache<String, Map<String, Object>> cache;
private final PrincipalAttributesCacheLoader cacheLoader =
new PrincipalAttributesCacheLoader();
/**
* Used for serialization only.
*/
private CachingPrincipalAttributesRepository() {
this.cache = null;
}
/**
* Instantiates a new caching attributes principal factory.
* Sets the default cache size to {@link #DEFAULT_MAXIMUM_CACHE_SIZE}.
* @param attributeRepository the attribute repository
* @param timeUnit the time unit
* @param expiryDuration the expiry duration
*/
public CachingPrincipalAttributesRepository(final IPersonAttributeDao attributeRepository,
final TimeUnit timeUnit,
final long expiryDuration) {
this(attributeRepository, DEFAULT_MAXIMUM_CACHE_SIZE, timeUnit, expiryDuration);
}
/**
* Instantiates a new caching attributes principal factory.
* @param attributeRepository the attribute repository
* @param maxCacheSize the max cache size
* @param timeUnit the time unit
* @param expiryDuration the expiry duration
*/
public CachingPrincipalAttributesRepository(final IPersonAttributeDao attributeRepository,
final long maxCacheSize,
final TimeUnit timeUnit,
final long expiryDuration) {
super(attributeRepository, expiryDuration, timeUnit);
this.cache = CacheBuilder.newBuilder().maximumSize(maxCacheSize)
.expireAfterWrite(expiryDuration, timeUnit).build(this.cacheLoader);
}
@Override
protected void addPrincipalAttributes(final String id, final Map<String, Object> attributes) {
this.cache.put(id, attributes);
logger.debug("Cached attributes for {}", id);
}
@Override
protected Map<String, Object> getPrincipalAttributes(final Principal p) {
try {
return this.cache.get(p.getId(), new Callable<Map<String, Object>>() {
@Override
public Map<String, Object> call() throws Exception {
logger.debug("No cached attributes could be found for {}", p.getId());
return new HashMap<>();
}
});
} catch (final Exception e) {
logger.error(e.getMessage(), e);
}
return null;
}
@Override
public void close() throws IOException {
this.cache.cleanUp();
}
private static class PrincipalAttributesCacheLoader extends CacheLoader<String, Map<String, Object>> {
@Override
public Map<String, Object> load(final String key) throws Exception {
return new HashMap<>();
}
}
}
......@@ -25,7 +25,6 @@ import org.jasig.cas.services.RegisteredServiceAccessStrategy;
import org.jasig.cas.services.RegisteredServiceProxyPolicy;
import org.jasig.cas.util.AbstractJacksonBackedJsonSerializer;
import javax.cache.expiry.Duration;
import java.net.URL;
import java.util.Map;
......@@ -52,26 +51,10 @@ public final class RegisteredServiceJsonSerializer extends AbstractJacksonBacked
final ObjectMapper mapper = super.initializeObjectMapper();
mapper.addMixIn(RegisteredServiceProxyPolicy.class, RegisteredServiceProxyPolicyMixin.class);
mapper.addMixIn(RegisteredServiceAccessStrategy.class, RegisteredServiceAuthorizationStrategyMixin.class);
mapper.addMixIn(Duration.class, DurationMixin.class);
return mapper;
}
private static class DurationMixin extends Duration {
private static final long serialVersionUID = 743505593336053306L;
@JsonIgnore
@Override
public boolean isEternal() {
return false;
}
@JsonIgnore
@Override
public boolean isZero() {
return false;
}
}
private static class RegisteredServiceProxyPolicyMixin implements RegisteredServiceProxyPolicy {
private static final long serialVersionUID = 4854597398304437341L;
......
......@@ -28,11 +28,11 @@ import org.jasig.cas.authentication.DefaultHandlerResult;
import org.jasig.cas.authentication.HttpBasedServiceCredential;
import org.jasig.cas.authentication.UsernamePasswordCredential;
import org.jasig.cas.authentication.handler.support.SimpleTestUsernamePasswordAuthenticationHandler;
import org.jasig.cas.authentication.principal.CachingPrincipalAttributesRepository;
import org.jasig.cas.authentication.principal.DefaultPrincipalFactory;
import org.jasig.cas.authentication.principal.Principal;
import org.jasig.cas.authentication.principal.Service;
import org.jasig.cas.authentication.principal.SimpleWebApplicationServiceImpl;
import org.jasig.cas.authentication.principal.cache.CachingPrincipalAttributesRepository;
import org.jasig.cas.services.AbstractRegisteredService;
import org.jasig.cas.services.DefaultRegisteredServiceAccessStrategy;
import org.jasig.cas.services.LogoutType;
......@@ -62,6 +62,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author Scott Battaglia
......@@ -170,7 +171,7 @@ public final class TestUtils {
policy.setAuthorizedToReleaseProxyGrantingTicket(true);
final CachingPrincipalAttributesRepository repo =
new CachingPrincipalAttributesRepository(new StubPersonAttributeDao(), 10);
new CachingPrincipalAttributesRepository(new StubPersonAttributeDao(), TimeUnit.SECONDS , 10);
repo.setMergingStrategy(new NoncollidingAttributeAdder());
policy.setPrincipalAttributesRepository(repo);
policy.setAttributeFilter(new RegisteredServiceRegexAttributeFilter("https://.+"));
......
......@@ -16,8 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.jasig.cas.authentication.principal;
package org.jasig.cas.authentication.principal.cache;
import org.jasig.cas.authentication.principal.DefaultPrincipalFactory;
import org.jasig.cas.authentication.principal.Principal;
import org.jasig.cas.authentication.principal.PrincipalFactory;
import org.jasig.services.persondir.IPersonAttributeDao;
import org.jasig.services.persondir.IPersonAttributes;
import org.jasig.services.persondir.support.merger.MultivaluedAttributeMerger;
......@@ -26,7 +30,6 @@ import org.jasig.services.persondir.support.merger.ReplacingAttributeAdder;
import org.junit.Before;
import org.junit.Test;
import java.io.Closeable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
......@@ -36,17 +39,19 @@ import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Handles tests for {@link CachingPrincipalAttributesRepository}.
* Parent class for test cases around {@link org.jasig.cas.authentication.principal.PrincipalAttributesRepository}.
* @author Misagh Moayyed
* @since 4.1
* @since 4.2
*/
public class CachingPrincipalAttributesRepositoryTests {
private Map<String, List<Object>> attributes;
public abstract class AbstractGuavaCachingPrincipalAttributesRepositoryTests {
protected IPersonAttributeDao dao;
private IPersonAttributeDao dao;
private Map<String, List<Object>> attributes;
private final PrincipalFactory principalFactory = new DefaultPrincipalFactory();
......@@ -72,65 +77,62 @@ public class CachingPrincipalAttributesRepositoryTests {
new ArrayList(Arrays.asList(new Object[]{"final@school.com"}))));
}
protected abstract AbstractPrincipalAttributesRepository getPrincipalAttributesRepository(TimeUnit unit, long duration);
@Test
public void checkExpiredCachedAttributes() throws Exception {
assertEquals(this.principal.getAttributes().size(), 1);
final PrincipalAttributesRepository repository = new CachingPrincipalAttributesRepository(this.dao,
TimeUnit.MILLISECONDS, 100);
assertEquals(repository.getAttributes(this.principal).size(), this.attributes.size());
assertTrue(repository.getAttributes(this.principal).containsKey("mail"));
Thread.sleep(200);
this.attributes.remove("mail");
assertTrue(repository.getAttributes(this.principal).containsKey("a2"));
assertFalse(repository.getAttributes(this.principal).containsKey("mail"));
((Closeable) repository).close();
try (final AbstractPrincipalAttributesRepository repository = getPrincipalAttributesRepository(TimeUnit.MILLISECONDS, 100)) {
assertEquals(repository.getAttributes(this.principal).size(), this.attributes.size());
assertTrue(repository.getAttributes(this.principal).containsKey("mail"));
Thread.sleep(200);
this.attributes.remove("mail");
assertTrue(repository.getAttributes(this.principal).containsKey("a2"));
assertFalse(repository.getAttributes(this.principal).containsKey("mail"));
}
}
@Test
public void ensureCachedAttributesWithUpdate() throws Exception {
final PrincipalAttributesRepository repository = new CachingPrincipalAttributesRepository(this.dao,
TimeUnit.SECONDS, 5);
assertEquals(repository.getAttributes(this.principal).size(), this.attributes.size());
assertTrue(repository.getAttributes(this.principal).containsKey("mail"));
attributes.clear();
assertTrue(repository.getAttributes(this.principal).containsKey("mail"));
((Closeable) repository).close();
try (final AbstractPrincipalAttributesRepository repository = getPrincipalAttributesRepository(TimeUnit.SECONDS, 5)) {
assertEquals(repository.getAttributes(this.principal).size(), this.attributes.size());
assertTrue(repository.getAttributes(this.principal).containsKey("mail"));
attributes.clear();
assertTrue(repository.getAttributes(this.principal).containsKey("mail"));
}
}
@Test
public void verifyMergingStrategyWithNoncollidingAttributeAdder() throws Exception {
final CachingPrincipalAttributesRepository repository = new CachingPrincipalAttributesRepository(this.dao,
TimeUnit.SECONDS, 5);
repository.setMergingStrategy(new NoncollidingAttributeAdder());
try (final AbstractPrincipalAttributesRepository repository = getPrincipalAttributesRepository(TimeUnit.SECONDS, 5)) {
repository.setMergingStrategy(new NoncollidingAttributeAdder());
assertTrue(repository.getAttributes(this.principal).containsKey("mail"));
assertEquals(repository.getAttributes(this.principal).get("mail").toString(), "final@school.com");
((Closeable) repository).close();
assertTrue(repository.getAttributes(this.principal).containsKey("mail"));
assertEquals(repository.getAttributes(this.principal).get("mail").toString(), "final@school.com");
}
}
@Test
public void verifyMergingStrategyWithReplacingAttributeAdder() throws Exception {
final CachingPrincipalAttributesRepository repository = new CachingPrincipalAttributesRepository(this.dao,
TimeUnit.SECONDS, 5);
repository.setMergingStrategy(new ReplacingAttributeAdder());
try (final AbstractPrincipalAttributesRepository repository = getPrincipalAttributesRepository(TimeUnit.SECONDS, 5)) {
repository.setMergingStrategy(new ReplacingAttributeAdder());
assertTrue(repository.getAttributes(this.principal).containsKey("mail"));
assertEquals(repository.getAttributes(this.principal).get("mail").toString(), "final@example.com");
((Closeable) repository).close();
assertTrue(repository.getAttributes(this.principal).containsKey("mail"));
assertEquals(repository.getAttributes(this.principal).get("mail").toString(), "final@example.com");
}
}
@Test
public void verifyMergingStrategyWithMultivaluedAttributeMerger() throws Exception {
final CachingPrincipalAttributesRepository repository = new CachingPrincipalAttributesRepository(this.dao,
TimeUnit.SECONDS, 5);
repository.setMergingStrategy(new MultivaluedAttributeMerger());
try (final AbstractPrincipalAttributesRepository repository = getPrincipalAttributesRepository(TimeUnit.SECONDS, 5)) {
repository.setMergingStrategy(new MultivaluedAttributeMerger());
assertTrue(repository.getAttributes(this.principal).get("mail") instanceof List);
assertTrue(repository.getAttributes(this.principal).get("mail") instanceof List);
final List<?> values = (List) repository.getAttributes(this.principal).get("mail");
assertTrue(values.contains("final@example.com"));
assertTrue(values.contains("final@school.com"));
((Closeable) repository).close();
final List<?> values = (List) repository.getAttributes(this.principal).get("mail");
assertTrue(values.contains("final@example.com"));
assertTrue(values.contains("final@school.com"));
}
}
}
/*
* 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:
*
* 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.cas.authentication.principal.cache;
import java.util.concurrent.TimeUnit;
/**
* Handles tests for {@link CachingPrincipalAttributesRepository}.
* @author Misagh Moayyed
* @since 4.1
*/
public class CachingPrincipalAttributesRepositoryTests extends AbstractGuavaCachingPrincipalAttributesRepositoryTests {
@Override
protected AbstractPrincipalAttributesRepository getPrincipalAttributesRepository(final TimeUnit unit, final long duration) {
return new CachingPrincipalAttributesRepository(this.dao,
unit, duration);
}
}
......@@ -19,10 +19,10 @@
package org.jasig.cas.services;
import org.apache.commons.lang3.SerializationUtils;
import org.jasig.cas.authentication.principal.CachingPrincipalAttributesRepository;
import org.jasig.cas.authentication.principal.DefaultPrincipalFactory;
import org.jasig.cas.authentication.principal.Principal;
import org.jasig.cas.authentication.principal.PrincipalAttributesRepository;
import org.jasig.cas.authentication.principal.cache.CachingPrincipalAttributesRepository;
import org.jasig.cas.services.support.RegisteredServiceRegexAttributeFilter;
import org.jasig.services.persondir.IPersonAttributeDao;
import org.jasig.services.persondir.IPersonAttributes;
......@@ -37,7 +37,8 @@ import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Attribute filtering policy tests.
......
......@@ -20,8 +20,8 @@ package org.jasig.cas.services;
import com.google.common.collect.Sets;
import org.apache.commons.io.FileUtils;
import org.jasig.cas.authentication.principal.CachingPrincipalAttributesRepository;
import org.jasig.cas.authentication.principal.ShibbolethCompatiblePersistentIdGenerator;
import org.jasig.cas.authentication.principal.cache.CachingPrincipalAttributesRepository;
import org.jasig.cas.services.support.RegisteredServiceRegexAttributeFilter;
import org.jasig.services.persondir.support.StubPersonAttributeDao;
import org.jasig.services.persondir.support.merger.ReplacingAttributeAdder;
......@@ -281,6 +281,36 @@ public class JsonServiceRegistryDaoTests {
assertEquals(r2.getAttributeReleasePolicy(), r3.getAttributeReleasePolicy());
}
@Test
public void verifySaveAttributeReleasePolicyAllowedAttrRulesWithGuavaCaching() {
final RegisteredServiceImpl r = new RegisteredServiceImpl();
r.setName("verifySaveAttributeReleasePolicyAllowedAttrRulesWithGuavaCaching");
r.setServiceId("testId");
final ReturnAllowedAttributeReleasePolicy policy = new ReturnAllowedAttributeReleasePolicy();
policy.setAllowedAttributes(Arrays.asList("1", "2", "3"));
final Map<String, List<Object>> attributes = new HashMap<>();
attributes.put("values", Arrays.asList(new Object[]{"v1", "v2", "v3"}));
final CachingPrincipalAttributesRepository repository =
new CachingPrincipalAttributesRepository(
new StubPersonAttributeDao(attributes),
TimeUnit.MILLISECONDS, 100);
repository.setMergingStrategy(new ReplacingAttributeAdder());
policy.setPrincipalAttributesRepository(repository);
r.setAttributeReleasePolicy(policy);
final RegisteredService r2 = this.dao.save(r);
final RegisteredService r3 = this.dao.findServiceById(r2.getId());
assertEquals(r, r2);
assertEquals(r2, r3);
assertNotNull(r3.getAttributeReleasePolicy());
assertEquals(r2.getAttributeReleasePolicy(), r3.getAttributeReleasePolicy());
}
@Test
public void verifyServiceRemovals() throws Exception{
final List<RegisteredService> list = new ArrayList<>(5);
......@@ -296,7 +326,7 @@ public class JsonServiceRegistryDaoTests {
for (final RegisteredService r2 : list) {
this.dao.delete(r2);
Thread.sleep(1);
Thread.sleep(500);
assertNull(this.dao.findServiceById(r2.getId()));
}
......
......@@ -49,7 +49,7 @@
<Logger name="org.slf4j.impl" level="trace">
<AppenderRef ref="slf4j"/>
</Logger>
<Logger name="org.jasig" level="warn">
<Logger name="org.jasig" level="warn" additivity="false">
<AppenderRef ref="console"/>
</Logger>
<Root level="warn">
......
......@@ -17,7 +17,8 @@ Attributes pass through a two-step process:
## Configuration
Once principal attributes are [resolved](Attribute-Resolution.html), adopters may choose to allow/release each attribute per each definition in the registry. Example configuration follows:
Once principal attributes are [resolved](Attribute-Resolution.html), adopters may choose
to allow/release each attribute per each definition in the registry. Example configuration follows:
{% highlight xml %}
<bean class="org.jasig.cas.services.RegisteredServiceImpl">
......@@ -41,13 +42,15 @@ Once principal attributes are [resolved](Attribute-Resolution.html), adopters ma
### Principal-Id Attribute
The service registry component of CAS has the ability to allow for configuration of a `usernameAttributeProvider` to be returned for the given registered service. When this property is set for a service, CAS will return the value of the configured attribute as part of its validation process.
The service registry component of CAS has the ability to allow for configuration of a
`usernameAttributeProvider` to be returned for the given registered service. When this property is set for a service, CAS will return the value of the configured attribute as part of its validation process.
* Ensure the attribute is available and resolved for the principal
* Set the `usernameAttributeProvider` property of the given service to once of the attribute providers below
####`DefaultRegisteredServiceUsernameProvider`
The default configuration which need not explicitly be defined, simply returns the resolved principal id as the username for this service.