Explorar o código

SEC-2114: Provide Spring Cache Abstraction based cache implementations

As of Spring 3.1 spring has its own cache abstraction. This commit adds cache
imlpementations based on that abstraction.
Marten Deinum %!s(int64=12) %!d(string=hai) anos
pai
achega
01ea39ce35

+ 145 - 0
acl/src/main/java/org/springframework/security/acls/domain/SpringCacheBasedAclCache.java

@@ -0,0 +1,145 @@
+/*
+ * Copyright 2002-2013 the original author or authors.
+ *
+ * Licensed 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.springframework.security.acls.domain;
+
+import net.sf.ehcache.CacheException;
+import net.sf.ehcache.Ehcache;
+import net.sf.ehcache.Element;
+import org.springframework.cache.Cache;
+import org.springframework.security.acls.model.AclCache;
+import org.springframework.security.acls.model.MutableAcl;
+import org.springframework.security.acls.model.ObjectIdentity;
+import org.springframework.security.acls.model.PermissionGrantingStrategy;
+import org.springframework.security.util.FieldUtils;
+import org.springframework.util.Assert;
+
+import java.io.Serializable;
+
+
+/**
+ * Simple implementation of {@link org.springframework.security.acls.model.AclCache} that delegates to {@link Cache} implementation.
+ * <p>
+ * Designed to handle the transient fields in {@link org.springframework.security.acls.domain.AclImpl}. Note that this implementation assumes all
+ * {@link org.springframework.security.acls.domain.AclImpl} instances share the same {@link org.springframework.security.acls.model.PermissionGrantingStrategy} and {@link org.springframework.security.acls.domain.AclAuthorizationStrategy}
+ * instances.
+ *
+ * @author Marten Deinum
+ * @since 3.2
+ */
+public class SpringCacheBasedAclCache implements AclCache {
+    //~ Instance fields ================================================================================================
+
+    private final Cache cache;
+    private PermissionGrantingStrategy permissionGrantingStrategy;
+    private AclAuthorizationStrategy aclAuthorizationStrategy;
+
+    //~ Constructors ===================================================================================================
+
+    public SpringCacheBasedAclCache(Cache cache, PermissionGrantingStrategy permissionGrantingStrategy,
+                                    AclAuthorizationStrategy aclAuthorizationStrategy) {
+        Assert.notNull(cache, "Cache required");
+        Assert.notNull(permissionGrantingStrategy, "PermissionGrantingStrategy required");
+        Assert.notNull(aclAuthorizationStrategy, "AclAuthorizationStrategy required");
+        this.cache = cache;
+        this.permissionGrantingStrategy = permissionGrantingStrategy;
+        this.aclAuthorizationStrategy = aclAuthorizationStrategy;
+    }
+
+    //~ Methods ========================================================================================================
+
+    public void evictFromCache(Serializable pk) {
+        Assert.notNull(pk, "Primary key (identifier) required");
+
+        MutableAcl acl = getFromCache(pk);
+
+        if (acl != null) {
+            cache.evict(acl.getId());
+            cache.evict(acl.getObjectIdentity());
+        }
+    }
+
+    public void evictFromCache(ObjectIdentity objectIdentity) {
+        Assert.notNull(objectIdentity, "ObjectIdentity required");
+
+        MutableAcl acl = getFromCache(objectIdentity);
+
+        if (acl != null) {
+            cache.evict(acl.getId());
+            cache.evict(acl.getObjectIdentity());
+        }
+    }
+
+    public MutableAcl getFromCache(ObjectIdentity objectIdentity) {
+        Assert.notNull(objectIdentity, "ObjectIdentity required");
+
+        Cache.ValueWrapper element = null;
+
+        try {
+            element = cache.get(objectIdentity);
+        } catch (CacheException ignored) {}
+
+        if (element == null) {
+            return null;
+        }
+
+        return initializeTransientFields((MutableAcl)element.get());
+    }
+
+    public MutableAcl getFromCache(Serializable pk) {
+        Assert.notNull(pk, "Primary key (identifier) required");
+
+        Cache.ValueWrapper element = null;
+
+        try {
+            element = cache.get(pk);
+        } catch (CacheException ignored) {}
+
+        if (element == null) {
+            return null;
+        }
+
+        return initializeTransientFields((MutableAcl) element.get());
+    }
+
+    public void putInCache(MutableAcl acl) {
+        Assert.notNull(acl, "Acl required");
+        Assert.notNull(acl.getObjectIdentity(), "ObjectIdentity required");
+        Assert.notNull(acl.getId(), "ID required");
+
+        if ((acl.getParentAcl() != null) && (acl.getParentAcl() instanceof MutableAcl)) {
+            putInCache((MutableAcl) acl.getParentAcl());
+        }
+
+        cache.put(acl.getObjectIdentity(), acl);
+        cache.put(acl.getId(), acl);
+    }
+
+    private MutableAcl initializeTransientFields(MutableAcl value) {
+        if (value instanceof AclImpl) {
+            FieldUtils.setProtectedFieldValue("aclAuthorizationStrategy", value, this.aclAuthorizationStrategy);
+            FieldUtils.setProtectedFieldValue("permissionGrantingStrategy", value, this.permissionGrantingStrategy);
+        }
+
+        if (value.getParentAcl() != null) {
+            initializeTransientFields((MutableAcl) value.getParentAcl());
+        }
+        return value;
+    }
+
+    public void clearCache() {
+        cache.clear();
+    }
+}

+ 172 - 0
acl/src/test/java/org/springframework/security/acls/jdbc/SpringCacheBasedAclCacheTests.java

@@ -0,0 +1,172 @@
+package org.springframework.security.acls.jdbc;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.springframework.cache.Cache;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
+import org.springframework.security.acls.domain.*;
+import org.springframework.security.acls.model.MutableAcl;
+import org.springframework.security.acls.model.ObjectIdentity;
+import org.springframework.security.acls.model.PermissionGrantingStrategy;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.util.FieldUtils;
+
+import java.io.*;
+import java.util.Map;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests {@link org.springframework.security.acls.domain.EhCacheBasedAclCache}
+ *
+ * @author Andrei Stefan
+ */
+public class SpringCacheBasedAclCacheTests {
+    private static final String TARGET_CLASS = "org.springframework.security.acls.TargetObject";
+
+    private static CacheManager cacheManager;
+
+    @BeforeClass
+    public static void initCacheManaer() {
+        cacheManager = new ConcurrentMapCacheManager();
+        // Use disk caching immediately (to test for serialization issue reported in SEC-527)
+        cacheManager.getCache("springcasebasedacltests");
+    }
+
+    @After
+    public void clearContext() {
+        SecurityContextHolder.clearContext();
+    }
+
+    private Cache getCache() {
+        Cache cache = cacheManager.getCache("springcasebasedacltests");
+        cache.clear();
+        return cache;
+    }
+
+    @Test(expected=IllegalArgumentException.class)
+    public void constructorRejectsNullParameters() throws Exception {
+        new SpringCacheBasedAclCache(null, null, null);
+    }
+
+    @Test
+    public void cacheOperationsAclWithoutParent() throws Exception {
+        Cache cache = getCache();
+        Map realCache = (Map) cache.getNativeCache();
+        ObjectIdentity identity = new ObjectIdentityImpl(TARGET_CLASS, Long.valueOf(100));
+        AclAuthorizationStrategy aclAuthorizationStrategy = new AclAuthorizationStrategyImpl(
+                new SimpleGrantedAuthority("ROLE_OWNERSHIP"), new SimpleGrantedAuthority("ROLE_AUDITING"),
+                new SimpleGrantedAuthority("ROLE_GENERAL"));
+        AuditLogger auditLogger = new ConsoleAuditLogger();
+
+        PermissionGrantingStrategy permissionGrantingStrategy = new DefaultPermissionGrantingStrategy(auditLogger);
+        SpringCacheBasedAclCache myCache = new SpringCacheBasedAclCache(cache, permissionGrantingStrategy, aclAuthorizationStrategy);
+        MutableAcl acl = new AclImpl(identity, Long.valueOf(1), aclAuthorizationStrategy, auditLogger);
+
+        assertEquals(0, realCache.size());
+        myCache.putInCache(acl);
+
+        // Check we can get from cache the same objects we put in
+        assertEquals(myCache.getFromCache(Long.valueOf(1)), acl);
+        assertEquals(myCache.getFromCache(identity), acl);
+
+        // Put another object in cache
+        ObjectIdentity identity2 = new ObjectIdentityImpl(TARGET_CLASS, Long.valueOf(101));
+        MutableAcl acl2 = new AclImpl(identity2, Long.valueOf(2), aclAuthorizationStrategy, new ConsoleAuditLogger());
+
+        myCache.putInCache(acl2);
+
+        // Try to evict an entry that doesn't exist
+        myCache.evictFromCache(Long.valueOf(3));
+        myCache.evictFromCache(new ObjectIdentityImpl(TARGET_CLASS, Long.valueOf(102)));
+        assertEquals(realCache.size(), 4);
+
+        myCache.evictFromCache(Long.valueOf(1));
+        assertEquals(realCache.size(), 2);
+
+        // Check the second object inserted
+        assertEquals(myCache.getFromCache(Long.valueOf(2)), acl2);
+        assertEquals(myCache.getFromCache(identity2), acl2);
+
+        myCache.evictFromCache(identity2);
+        assertEquals(realCache.size(), 0);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void cacheOperationsAclWithParent() throws Exception {
+        Cache cache = getCache();
+        Map realCache = (Map) cache.getNativeCache();
+
+        Authentication auth = new TestingAuthenticationToken("user", "password", "ROLE_GENERAL");
+        auth.setAuthenticated(true);
+        SecurityContextHolder.getContext().setAuthentication(auth);
+
+        ObjectIdentity identity = new ObjectIdentityImpl(TARGET_CLASS, Long.valueOf(1));
+        ObjectIdentity identityParent = new ObjectIdentityImpl(TARGET_CLASS, Long.valueOf(2));
+        AclAuthorizationStrategy aclAuthorizationStrategy = new AclAuthorizationStrategyImpl(
+                new SimpleGrantedAuthority("ROLE_OWNERSHIP"), new SimpleGrantedAuthority("ROLE_AUDITING"),
+                new SimpleGrantedAuthority("ROLE_GENERAL"));
+        AuditLogger auditLogger = new ConsoleAuditLogger();
+
+        PermissionGrantingStrategy permissionGrantingStrategy = new DefaultPermissionGrantingStrategy(auditLogger);
+        SpringCacheBasedAclCache myCache = new SpringCacheBasedAclCache(cache, permissionGrantingStrategy, aclAuthorizationStrategy);
+
+        MutableAcl acl = new AclImpl(identity, Long.valueOf(1), aclAuthorizationStrategy, auditLogger);
+        MutableAcl parentAcl = new AclImpl(identityParent, Long.valueOf(2), aclAuthorizationStrategy, auditLogger);
+
+        acl.setParent(parentAcl);
+
+        assertEquals(0, realCache.size());
+        myCache.putInCache(acl);
+        assertEquals(realCache.size(), 4);
+
+        // Check we can get from cache the same objects we put in
+        AclImpl aclFromCache = (AclImpl) myCache.getFromCache(Long.valueOf(1));
+        assertEquals(acl, aclFromCache);
+        // SEC-951 check transient fields are set on parent
+        assertNotNull(FieldUtils.getFieldValue(aclFromCache.getParentAcl(), "aclAuthorizationStrategy"));
+        assertNotNull(FieldUtils.getFieldValue(aclFromCache.getParentAcl(), "permissionGrantingStrategy"));
+        assertEquals(acl, myCache.getFromCache(identity));
+        assertNotNull(FieldUtils.getFieldValue(aclFromCache, "aclAuthorizationStrategy"));
+        AclImpl parentAclFromCache = (AclImpl) myCache.getFromCache(Long.valueOf(2));
+        assertEquals(parentAcl, parentAclFromCache);
+        assertNotNull(FieldUtils.getFieldValue(parentAclFromCache, "aclAuthorizationStrategy"));
+        assertEquals(parentAcl, myCache.getFromCache(identityParent));
+    }
+
+    //~ Inner Classes ==================================================================================================
+
+    private class MockCache implements Cache {
+
+        @Override
+        public String getName() {
+            return "mockcache";
+        }
+
+        @Override
+        public Object getNativeCache() {
+            return null;
+        }
+
+        @Override
+        public ValueWrapper get(Object key) {
+            return null;
+        }
+
+        @Override
+        public void put(Object key, Object value) {}
+
+        @Override
+        public void evict(Object key) {}
+
+        @Override
+        public void clear() {}
+    }
+}

+ 86 - 0
cas/src/main/java/org/springframework/security/cas/authentication/SpringCacheBasedTicketCache.java

@@ -0,0 +1,86 @@
+/*
+ * Copyright 2002-2013 the original author or authors.
+ *
+ * Licensed 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.springframework.security.cas.authentication;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.cache.Cache;
+import org.springframework.util.Assert;
+
+
+/**
+ * Caches tickets using a Spring IoC defined {@link Cache}.
+ *
+ * @author Marten Deinum
+ * @since 3.2
+ *
+ */
+public class SpringCacheBasedTicketCache implements StatelessTicketCache, InitializingBean {
+    //~ Static fields/initializers =====================================================================================
+
+    private static final Log logger = LogFactory.getLog(SpringCacheBasedTicketCache.class);
+
+    //~ Instance fields ================================================================================================
+
+    private Cache cache;
+
+    //~ Methods ========================================================================================================
+
+    public void afterPropertiesSet() throws Exception {
+        Assert.notNull(cache, "cache mandatory");
+    }
+
+    public CasAuthenticationToken getByTicketId(final String serviceTicket) {
+        final Cache.ValueWrapper element = serviceTicket != null ? cache.get(serviceTicket) : null;
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Cache hit: " + (element != null) + "; service ticket: " + serviceTicket);
+        }
+
+        return element == null ? null : (CasAuthenticationToken) element.get();
+    }
+
+    public Cache getCache() {
+        return cache;
+    }
+
+    public void putTicketInCache(final CasAuthenticationToken token) {
+        String key = token.getCredentials().toString();
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Cache put: " + key);
+        }
+
+        cache.put(key, token);
+    }
+
+    public void removeTicketFromCache(final CasAuthenticationToken token) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("Cache remove: " + token.getCredentials().toString());
+        }
+
+        this.removeTicketFromCache(token.getCredentials().toString());
+    }
+
+    public void removeTicketFromCache(final String serviceTicket) {
+        cache.evict(serviceTicket);
+    }
+
+    public void setCache(final Cache cache) {
+        this.cache = cache;
+    }
+}

+ 80 - 0
cas/src/test/java/org/springframework/security/cas/authentication/SpringCacheBasedTicketCacheTests.java

@@ -0,0 +1,80 @@
+/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
+ *
+ * Licensed 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.springframework.security.cas.authentication;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.springframework.cache.Cache;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests {@link org.springframework.security.cas.authentication.SpringCacheBasedTicketCache}.
+ *
+ * @author Marten Deinum
+ * @since 3.2
+ */
+public class SpringCacheBasedTicketCacheTests extends AbstractStatelessTicketCacheTests {
+    private static CacheManager cacheManager;
+
+    //~ Methods ========================================================================================================
+    @BeforeClass
+    public static void initCacheManaer() {
+        cacheManager = new ConcurrentMapCacheManager();
+        cacheManager.getCache("castickets");
+    }
+
+    @Test
+    public void testCacheOperation() throws Exception {
+        SpringCacheBasedTicketCache cache = new SpringCacheBasedTicketCache();
+        cache.setCache(cacheManager.getCache("castickets"));
+        cache.afterPropertiesSet();
+
+        final CasAuthenticationToken token = getToken();
+
+        // Check it gets stored in the cache
+        cache.putTicketInCache(token);
+        assertEquals(token, cache.getByTicketId("ST-0-ER94xMJmn6pha35CQRoZ"));
+
+        // Check it gets removed from the cache
+        cache.removeTicketFromCache(getToken());
+        assertNull(cache.getByTicketId("ST-0-ER94xMJmn6pha35CQRoZ"));
+
+        // Check it doesn't return values for null or unknown service tickets
+        assertNull(cache.getByTicketId(null));
+        assertNull(cache.getByTicketId("UNKNOWN_SERVICE_TICKET"));
+    }
+
+    @Test
+    public void testStartupDetectsMissingCache() throws Exception {
+        SpringCacheBasedTicketCache cache = new SpringCacheBasedTicketCache();
+
+        try {
+            cache.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        Cache myCache = cacheManager.getCache("castickets");
+        cache.setCache(myCache);
+        assertEquals(myCache, cache.getCache());
+    }
+}

+ 74 - 0
core/src/main/java/org/springframework/security/core/userdetails/cache/SpringCacheBasedUserCache.java

@@ -0,0 +1,74 @@
+package org.springframework.security.core.userdetails.cache;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.cache.Cache;
+import org.springframework.security.core.userdetails.UserCache;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.util.Assert;
+
+/**
+ * Caches {@link UserDetails} intances in a Spring defined {@link Cache}.
+ *
+ * @author Marten Deinum
+ * @since 3.2
+ */
+public class SpringCacheBasedUserCache implements UserCache, InitializingBean {
+
+
+    //~ Static fields/initializers =====================================================================================
+
+    private static final Log logger = LogFactory.getLog(SpringCacheBasedUserCache.class);
+
+    //~ Instance fields ================================================================================================
+
+    private Cache cache;
+
+    //~ Methods ========================================================================================================
+
+    public void afterPropertiesSet() throws Exception {
+        Assert.notNull(cache, "cache mandatory");
+    }
+
+    public Cache getCache() {
+        return cache;
+    }
+
+    public UserDetails getUserFromCache(String username) {
+        Cache.ValueWrapper element = username != null ? cache.get(username) : null;
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Cache hit: " + (element != null) + "; username: " + username);
+        }
+
+        if (element == null) {
+            return null;
+        } else {
+            return (UserDetails) element.get();
+        }
+    }
+
+    public void putUserInCache(UserDetails user) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("Cache put: " + user.getUsername());
+        }
+        cache.put(user.getUsername(), user);
+    }
+
+    public void removeUserFromCache(UserDetails user) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("Cache remove: " + user.getUsername());
+        }
+
+        this.removeUserFromCache(user.getUsername());
+    }
+
+    public void removeUserFromCache(String username) {
+        cache.evict(username);
+    }
+
+    public void setCache(Cache cache) {
+        this.cache = cache;
+    }
+}

+ 92 - 0
core/src/test/java/org/springframework/security/core/userdetails/cache/SpringCacheBasedUserCacheTests.java

@@ -0,0 +1,92 @@
+/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
+ *
+ * Licensed 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.springframework.security.core.userdetails.cache;
+
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.springframework.cache.Cache;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.userdetails.User;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests {@link org.springframework.security.core.userdetails.cache.SpringCacheBasedUserCache}.
+ *
+ * @author Marten Deinum
+ * @since 3.2
+ *
+ */
+public class SpringCacheBasedUserCacheTests {
+    private static CacheManager cacheManager;
+
+    //~ Methods ========================================================================================================
+    @BeforeClass
+    public static void initCacheManaer() {
+        cacheManager = new ConcurrentMapCacheManager();
+        cacheManager.getCache("springbasedusercachetests");
+    }
+
+    @AfterClass
+    public static void shutdownCacheManager() {
+    }
+
+    private Cache getCache() {
+        Cache cache = cacheManager.getCache("springbasedusercachetests");
+        cache.clear();
+        return cache;
+    }
+
+    private User getUser() {
+        return new User("john", "password", true, true, true, true,
+                AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO"));
+    }
+
+    @Test
+    public void cacheOperationsAreSuccessful() throws Exception {
+        SpringCacheBasedUserCache cache = new SpringCacheBasedUserCache();
+        cache.setCache(getCache());
+        cache.afterPropertiesSet();
+
+        // Check it gets stored in the cache
+        cache.putUserInCache(getUser());
+        assertEquals(getUser().getPassword(), cache.getUserFromCache(getUser().getUsername()).getPassword());
+
+        // Check it gets removed from the cache
+        cache.removeUserFromCache(getUser());
+        assertNull(cache.getUserFromCache(getUser().getUsername()));
+
+        // Check it doesn't return values for null or unknown users
+        assertNull(cache.getUserFromCache(null));
+        assertNull(cache.getUserFromCache("UNKNOWN_USER"));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void startupDetectsMissingCache() throws Exception {
+        SpringCacheBasedUserCache cache = new SpringCacheBasedUserCache();
+
+        cache.afterPropertiesSet();
+        fail("Should have thrown IllegalArgumentException");
+
+        Cache myCache = getCache();
+        cache.setCache(myCache);
+        assertEquals(myCache, cache.getCache());
+    }
+}