Skip to content

Commit

Permalink
Merge e81a50a into 38879e6
Browse files Browse the repository at this point in the history
  • Loading branch information
evnm committed Jul 17, 2016
2 parents 38879e6 + e81a50a commit f237ccd
Show file tree
Hide file tree
Showing 2 changed files with 309 additions and 0 deletions.
@@ -0,0 +1,182 @@
package io.dropwizard.auth;

import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.google.common.base.Predicate;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheBuilderSpec;
import com.google.common.cache.CacheStats;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.tuple.ImmutablePair;

import java.security.Principal;

import static com.codahale.metrics.MetricRegistry.name;

/**
* An {@link Authorizer} decorator which uses a Guava {@link Cache} to
* temporarily cache principals' role associations.
*
* Cache entries include both inclusion and exclusion of a principal
* within a given role.
*
* @param <P> the type of principals on which the authorizer operates
*/
public class CachingAuthorizer<P extends Principal> implements Authorizer<P> {
private final Authorizer<P> underlying;
private final Meter cacheMisses;
private final Timer getsTimer;

// A cache which maps (principal, role) tuples to boolean
// authorization states.
//
// A cached value of `true` indicates that the key's principal is
// authorized to assume the given role. False values indicate the
// principal is not authorized to assume the role.
//
// `null` cache values are interpreted as cache misses, and will
// thus result in read through to the underlying `Authorizer`.
private final Cache<ImmutablePair<P, String>, Boolean> cache;

/**
* Creates a new cached authorizer.
*
* @param metricRegistry the application's registry of metrics
* @param authorizer the underlying authorizer
* @param cacheSpec {@link CacheBuilderSpec}
*/
public CachingAuthorizer(
final MetricRegistry metricRegistry,
final Authorizer<P> authorizer,
final CacheBuilderSpec cacheSpec
) {
this(metricRegistry, authorizer, CacheBuilder.from(cacheSpec));
}

/**
* Creates a new cached authorizer.
*
* @param metricRegistry the application's registry of metrics
* @param authorizer the underlying authorizer
* @param builder a {@link CacheBuilder}
*/
public CachingAuthorizer(
final MetricRegistry metricRegistry,
final Authorizer<P> authorizer,
final CacheBuilder<Object, Object> builder
) {
this.underlying = authorizer;
this.cacheMisses = metricRegistry.meter(name(authorizer.getClass(), "cache-misses"));
this.getsTimer = metricRegistry.timer(name(authorizer.getClass(), "gets"));
this.cache = builder.recordStats().build();
}

@Override
public boolean authorize(P principal, String role) {
final Timer.Context context = getsTimer.time();

try {
final ImmutablePair<P, String> cacheKey = ImmutablePair.of(principal, role);
Boolean isAuthorized = cache.getIfPresent(cacheKey);

if (isAuthorized == null) {
cacheMisses.mark();
isAuthorized = Boolean.valueOf(underlying.authorize(principal, role));
cache.put(cacheKey, isAuthorized);
}

return isAuthorized.booleanValue();
} finally {
context.stop();
}
}

/**
* Discards any cached role associations for the given principal and role.
*
* @param principal
* @param role
*/
public void invalidate(P principal, String role) {
cache.invalidate(ImmutablePair.of(principal, role));
}

/**
* Discards any cached role associations for the given principal.
*
* @param principal
*/
public void invalidate(P principal) {
final Predicate<ImmutablePair<P, String>> predicate =
new Predicate<ImmutablePair<P, String>>() {
@Override public boolean apply(ImmutablePair<P, String> cacheKey) {
return cacheKey.getLeft().equals(principal);
}
};

cache.invalidateAll(Sets.filter(cache.asMap().keySet(), predicate));
}

/**
* Discards any cached role associations for the given collection
* of principals.
*
* @param principals a list of principals
*/
public void invalidateAll(Iterable<P> principals) {
final Predicate<ImmutablePair<P, String>> predicate =
new Predicate<ImmutablePair<P, String>>() {
@Override public boolean apply(ImmutablePair<P, String> cacheKey) {
return Iterables.contains(principals, cacheKey.getLeft());
}
};

cache.invalidateAll(Sets.filter(cache.asMap().keySet(), predicate));
}

/**
* Discards any cached role associations for principals satisfying
* the given predicate.
*
* @param predicate a predicate to filter credentials
*/
public void invalidateAll(Predicate<? super P> predicate) {
final Predicate<ImmutablePair<P, String>> nestedPredicate =
new Predicate<ImmutablePair<P, String>>() {
@Override public boolean apply(ImmutablePair<P, String> cacheKey) {
return predicate.apply(cacheKey.getLeft());
}
};

cache.invalidateAll(Sets.filter(cache.asMap().keySet(), nestedPredicate));
}

/**
* Discards all cached role associations.
*/
public void invalidateAll() {
cache.invalidateAll();
}

/**
* Returns the number of principals for which there are cached
* role associations.
*
* @return the number of cached principals
*/
public long size() {
return cache.size();
}

/**
* Returns a set of statistics about the cache contents and usage.
*
* @return a set of statistics about the cache contents and usage
*/
public CacheStats stats() {
return cache.stats();
}
}
@@ -0,0 +1,127 @@
package io.dropwizard.auth;

import com.codahale.metrics.MetricRegistry;
import com.google.common.cache.CacheBuilderSpec;
import com.google.common.collect.ImmutableSet;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;

import java.security.Principal;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.anyObject;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class CachingAuthorizerTest {
@SuppressWarnings("unchecked")
private final Authorizer<Principal> underlying = mock(Authorizer.class);
private final CachingAuthorizer<Principal> cached = new CachingAuthorizer<>(
new MetricRegistry(),
underlying,
CacheBuilderSpec.parse("maximumSize=1")
);

private final Principal principal = new PrincipalImpl("principal");
private final Principal principal2 = new PrincipalImpl("principal2");
private final Principal principal3 = new PrincipalImpl("principal3");
private final String role = "popular_kids";

@Before
public void setUp() throws Exception {
when(underlying.authorize(anyObject(), anyString())).thenReturn(true);
}

@Test
public void cachesTheFirstReturnedPrincipal() throws Exception {
assertThat(cached.authorize(principal, role)).isTrue();
assertThat(cached.authorize(principal, role)).isTrue();

verify(underlying, times(1)).authorize(principal, role);
}

@Test
public void respectsTheCacheConfiguration() throws Exception {
cached.authorize(principal, role);
cached.authorize(principal2, role);
cached.authorize(principal, role);

final InOrder inOrder = inOrder(underlying);
inOrder.verify(underlying, times(1)).authorize(principal, role);
inOrder.verify(underlying, times(1)).authorize(principal2, role);
inOrder.verify(underlying, times(1)).authorize(principal, role);
}

@Test
public void invalidatesPrincipalAndRole() throws Exception {
cached.authorize(principal, role);
cached.invalidate(principal, role);
cached.authorize(principal, role);

verify(underlying, times(2)).authorize(principal, role);
}

@Test
public void invalidatesSinglePrincipal() throws Exception {
cached.authorize(principal, role);
cached.invalidate(principal);
cached.authorize(principal, role);

verify(underlying, times(2)).authorize(principal, role);
}

@Test
public void invalidatesSetsofPrincipals() throws Exception {
cached.authorize(principal, role);
cached.authorize(principal2, role);
cached.invalidateAll(ImmutableSet.of(principal, principal2));
cached.authorize(principal, role);
cached.authorize(principal2, role);

verify(underlying, times(2)).authorize(principal, role);
verify(underlying, times(2)).authorize(principal2, role);
}

@Test
public void invalidatesPrincipalsMatchingGivenPredicate() throws Exception {
cached.authorize(principal, role);
cached.invalidateAll(principal::equals);
cached.authorize(principal, role);

verify(underlying, times(2)).authorize(principal, role);
}

@Test
public void invalidatesAllPrincipals() throws Exception {
cached.authorize(principal, role);
cached.authorize(principal2, role);
cached.invalidateAll();
cached.authorize(principal, role);
cached.authorize(principal2, role);

verify(underlying, times(2)).authorize(principal, role);
verify(underlying, times(2)).authorize(principal2, role);
}

@Test
public void calculatesTheSizeOfTheCache() throws Exception {
assertThat(cached.size()).isEqualTo(0);
cached.authorize(principal, role);
assertThat(cached.size()).isEqualTo(1);
cached.invalidateAll();
assertThat(cached.size()).isEqualTo(0);
}

@Test
public void calculatesCacheStats() throws Exception {
cached.authorize(principal, role);
assertThat(cached.stats().loadCount()).isEqualTo(0);
assertThat(cached.size()).isEqualTo(1);
}
}

0 comments on commit f237ccd

Please sign in to comment.