diff --git a/acl/src/main/java/org/springframework/security/acls/domain/SpringCacheBasedAclCache.java b/acl/src/main/java/org/springframework/security/acls/domain/SpringCacheBasedAclCache.java new file mode 100644 index 00000000000..87dba1873cd --- /dev/null +++ b/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. + *
+ * 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(); + } +} diff --git a/acl/src/test/java/org/springframework/security/acls/jdbc/SpringCacheBasedAclCacheTests.java b/acl/src/test/java/org/springframework/security/acls/jdbc/SpringCacheBasedAclCacheTests.java new file mode 100644 index 00000000000..05ced8ae67c --- /dev/null +++ b/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() {} + } +} diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/SpringCacheBasedTicketCache.java b/cas/src/main/java/org/springframework/security/cas/authentication/SpringCacheBasedTicketCache.java new file mode 100644 index 00000000000..caddb8f89d7 --- /dev/null +++ b/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; + } +} diff --git a/cas/src/test/java/org/springframework/security/cas/authentication/SpringCacheBasedTicketCacheTests.java b/cas/src/test/java/org/springframework/security/cas/authentication/SpringCacheBasedTicketCacheTests.java new file mode 100644 index 00000000000..40dd9f49ae3 --- /dev/null +++ b/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()); + } +} diff --git a/core/src/main/java/org/springframework/security/core/userdetails/cache/SpringCacheBasedUserCache.java b/core/src/main/java/org/springframework/security/core/userdetails/cache/SpringCacheBasedUserCache.java new file mode 100644 index 00000000000..b2625a2946d --- /dev/null +++ b/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; + } +} diff --git a/core/src/test/java/org/springframework/security/core/userdetails/cache/SpringCacheBasedUserCacheTests.java b/core/src/test/java/org/springframework/security/core/userdetails/cache/SpringCacheBasedUserCacheTests.java new file mode 100644 index 00000000000..4045d8bcf94 --- /dev/null +++ b/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()); + } +}