From 67ab7716dcf15c44ba4f9a0076bcbf17ffe5995e Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Thu, 10 Jul 2014 12:51:59 +0200 Subject: [PATCH] Added example for SpringSecurity integration. Fixed some Java 6 incompatibilities. Added mention for the security module in readme. Original pull request: #1. --- README.md | 1 + jpa/pom.xml | 1 + jpa/security/pom.xml | 22 ++---- .../jpa/security/BusinessObject.java | 62 ++++++++++++++++ .../security/BusinessObjectRepository.java | 27 +++++++ .../SecureBusinessObjectRepository.java | 46 ++++++++++++ .../jpa/security/SecurityConfiguration.java | 8 ++- .../SecurityEvaluationContextExtension.java | 15 ++-- .../jpa/security/{Customer.java => User.java} | 27 +++++-- ...merRepository.java => UserRepository.java} | 7 +- .../security/SecurityIntegrationTests.java | 70 ++++++++++++++++++- jpa/security/src/test/resources/logback.xml | 2 +- 12 files changed, 249 insertions(+), 39 deletions(-) create mode 100644 jpa/security/src/main/java/example/springdata/jpa/security/BusinessObject.java create mode 100644 jpa/security/src/main/java/example/springdata/jpa/security/BusinessObjectRepository.java create mode 100644 jpa/security/src/main/java/example/springdata/jpa/security/SecureBusinessObjectRepository.java rename jpa/security/src/main/java/example/springdata/jpa/security/{Customer.java => User.java} (67%) rename jpa/security/src/main/java/example/springdata/jpa/security/{CustomerRepository.java => UserRepository.java} (82%) diff --git a/README.md b/README.md index 055bae43..da2cd149 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ We have separate folders for the samples of individual modules: * `java8` - Example of how to use Spring Data JPA auditing with Java 8 date time types as well as the usage of `Optional` as return type for repository methods. Note, this project requires to be build with JDK 8. * `showcase` - Refactoring show case of how to improve a plain-JPA-based persistence layer by using Spring Data JPA (read: removing close to all of the implementation code). Follow the `demo.txt` file for detailed instructions. * `interceptors` - Example of how to enrich the repositories with AOP. +* `security` - Example of how to integrate Spring Data JPA Repositories with Spring Security. ## Spring Data MongoDB diff --git a/jpa/pom.xml b/jpa/pom.xml index 4d11ac62..4c983304 100644 --- a/jpa/pom.xml +++ b/jpa/pom.xml @@ -22,6 +22,7 @@ interceptors java8 jpa21 + security diff --git a/jpa/security/pom.xml b/jpa/security/pom.xml index 94e21e16..c7fb5d5e 100644 --- a/jpa/security/pom.xml +++ b/jpa/security/pom.xml @@ -11,23 +11,9 @@ spring-data-jpa-security Spring Data JPA - Spring Security integration - - - - - org.springframework.data - spring-data-jpa - 1.7.0.DATAJPA-564-SNAPSHOT - - - - org.springframework.data - spring-data-commons - 1.9.0.DATACMNS-533-SNAPSHOT - - - - + + Evans-BUILD-SNAPSHOT + @@ -36,4 +22,4 @@ - \ No newline at end of file + diff --git a/jpa/security/src/main/java/example/springdata/jpa/security/BusinessObject.java b/jpa/security/src/main/java/example/springdata/jpa/security/BusinessObject.java new file mode 100644 index 00000000..ceccb8f7 --- /dev/null +++ b/jpa/security/src/main/java/example/springdata/jpa/security/BusinessObject.java @@ -0,0 +1,62 @@ +/* + * Copyright 2014 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 example.springdata.jpa.security; + +import javax.persistence.*; + +/** + * @author Thomas Darimont + */ +@Entity +public class BusinessObject { + + @Id @GeneratedValue Long id; + String data; + + + @ManyToOne + User owner; + + public BusinessObject(String data, User owner) { + + this.data = data; + this.owner = owner; + } + + @Override + public boolean equals(Object o) { + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + BusinessObject that = (BusinessObject) o; + + if (data != null ? !data.equals(that.data) : that.data != null) return false; + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (owner != null ? !owner.equals(that.owner) : that.owner != null) return false; + + return true; + } + + @Override + public int hashCode() { + + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (data != null ? data.hashCode() : 0); + result = 31 * result + (owner != null ? owner.hashCode() : 0); + return result; + } +} diff --git a/jpa/security/src/main/java/example/springdata/jpa/security/BusinessObjectRepository.java b/jpa/security/src/main/java/example/springdata/jpa/security/BusinessObjectRepository.java new file mode 100644 index 00000000..a77f81fe --- /dev/null +++ b/jpa/security/src/main/java/example/springdata/jpa/security/BusinessObjectRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2014 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 example.springdata.jpa.security; + +import org.springframework.data.repository.CrudRepository; + +/** + * Repository to manage {@link BusinessObject} instances. + * + * @author Thomas Darimont + */ +public interface BusinessObjectRepository extends CrudRepository { + +} diff --git a/jpa/security/src/main/java/example/springdata/jpa/security/SecureBusinessObjectRepository.java b/jpa/security/src/main/java/example/springdata/jpa/security/SecureBusinessObjectRepository.java new file mode 100644 index 00000000..e08a65ab --- /dev/null +++ b/jpa/security/src/main/java/example/springdata/jpa/security/SecureBusinessObjectRepository.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014 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 example.springdata.jpa.security; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; + +/** + * @author Thomas Darimont + */ +public interface SecureBusinessObjectRepository extends Repository{ + + /** + * Here we demonstrate the usage of SpEL expression within a custom query. + * With the {@link example.springdata.jpa.security.SecurityEvaluationContextExtension} in place + * we can safely access auth information provided by the Spring Security Context. + * + * The Spring Data Repository infrastructure will translate the given query string into the + * parameterized form: + * + * + * select o from BusinessObject o where o.owner.emailAddress like ? + * + * + * and set the the result SpEL expression evaluated at method invocation time as parameter value. + * + * @return + */ + @Query("select o from BusinessObject o where o.owner.emailAddress like ?#{hasRole('ROLE_ADMIN') ? '%' : principal.emailAddress}") + List findBusinessObjectsForCurrentUser(); +} diff --git a/jpa/security/src/main/java/example/springdata/jpa/security/SecurityConfiguration.java b/jpa/security/src/main/java/example/springdata/jpa/security/SecurityConfiguration.java index 5135db6b..209f1e3c 100644 --- a/jpa/security/src/main/java/example/springdata/jpa/security/SecurityConfiguration.java +++ b/jpa/security/src/main/java/example/springdata/jpa/security/SecurityConfiguration.java @@ -18,14 +18,18 @@ package example.springdata.jpa.security; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.repository.query.spi.EvaluationContextExtensionSupport; +import org.springframework.data.repository.query.spi.EvaluationContextExtension; +/** + * @author Oliver Gierke + * @author Thomas Darimont + */ @Configuration @EnableAutoConfiguration class SecurityConfiguration { @Bean - EvaluationContextExtensionSupport securityExtension() { + EvaluationContextExtension securityExtension() { return new SecurityEvaluationContextExtension(); } } diff --git a/jpa/security/src/main/java/example/springdata/jpa/security/SecurityEvaluationContextExtension.java b/jpa/security/src/main/java/example/springdata/jpa/security/SecurityEvaluationContextExtension.java index 7b2f48e9..12f36d3a 100644 --- a/jpa/security/src/main/java/example/springdata/jpa/security/SecurityEvaluationContextExtension.java +++ b/jpa/security/src/main/java/example/springdata/jpa/security/SecurityEvaluationContextExtension.java @@ -15,15 +15,16 @@ */ package example.springdata.jpa.security; -import java.util.Collections; -import java.util.Map; - import org.springframework.data.repository.query.spi.EvaluationContextExtension; import org.springframework.data.repository.query.spi.EvaluationContextExtensionSupport; +import org.springframework.security.access.expression.SecurityExpressionRoot; import org.springframework.security.core.context.SecurityContextHolder; /** * {@link EvaluationContextExtension} to expose Spring Security's principal via a SpEL property. + * + * Note that this is just for the sake of demonstration - Spring Security will eventually provide its + * own EvaluationContext extension. * * @author Oliver Gierke */ @@ -38,12 +39,12 @@ class SecurityEvaluationContextExtension extends EvaluationContextExtensionSuppo return "security"; } - /* + /* * (non-Javadoc) - * @see org.springframework.data.repository.query.spi.EvaluationContextExtensionSupport#getProperties() + * @see org.springframework.data.repository.query.spi.EvaluationContextExtension#getRootObject() */ @Override - public Map getProperties() { - return Collections.singletonMap("principal", SecurityContextHolder.getContext().getAuthentication().getPrincipal()); + public SecurityExpressionRoot getRootObject() { + return new SecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication()) {}; } } diff --git a/jpa/security/src/main/java/example/springdata/jpa/security/Customer.java b/jpa/security/src/main/java/example/springdata/jpa/security/User.java similarity index 67% rename from jpa/security/src/main/java/example/springdata/jpa/security/Customer.java rename to jpa/security/src/main/java/example/springdata/jpa/security/User.java index 2639db5d..1f7fe393 100644 --- a/jpa/security/src/main/java/example/springdata/jpa/security/Customer.java +++ b/jpa/security/src/main/java/example/springdata/jpa/security/User.java @@ -20,17 +20,36 @@ import javax.persistence.GeneratedValue; import javax.persistence.Id; /** - * @author Oliver Gierke + * @author Thomas Darimont */ @Entity -public class Customer { +public class User { @Id @GeneratedValue Long id; - String firstname, lastname; - public Customer(String firstname, String lastname) { + String firstname; + String lastname; + String emailAddress; + public User(String firstname, String lastname, String emailAddress) { this.firstname = firstname; this.lastname = lastname; + this.emailAddress = emailAddress; + } + + public Long getId() { + return id; + } + + public String getFirstname() { + return firstname; + } + + public String getLastname() { + return lastname; + } + + public String getEmailAddress() { + return emailAddress; } } diff --git a/jpa/security/src/main/java/example/springdata/jpa/security/CustomerRepository.java b/jpa/security/src/main/java/example/springdata/jpa/security/UserRepository.java similarity index 82% rename from jpa/security/src/main/java/example/springdata/jpa/security/CustomerRepository.java rename to jpa/security/src/main/java/example/springdata/jpa/security/UserRepository.java index 8bf14c4e..267800e4 100644 --- a/jpa/security/src/main/java/example/springdata/jpa/security/CustomerRepository.java +++ b/jpa/security/src/main/java/example/springdata/jpa/security/UserRepository.java @@ -18,10 +18,7 @@ package example.springdata.jpa.security; import org.springframework.data.repository.CrudRepository; /** - * Repository to manage {@link Customer} instances. - * - * @author Oliver Gierke + * @author Thomas Darimont */ -public interface CustomerRepository extends CrudRepository { - +public interface UserRepository extends CrudRepository{ } diff --git a/jpa/security/src/test/java/example/springdata/jpa/security/SecurityIntegrationTests.java b/jpa/security/src/test/java/example/springdata/jpa/security/SecurityIntegrationTests.java index 2ad10155..7bb47579 100644 --- a/jpa/security/src/test/java/example/springdata/jpa/security/SecurityIntegrationTests.java +++ b/jpa/security/src/test/java/example/springdata/jpa/security/SecurityIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2014 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. @@ -15,19 +15,85 @@ */ package example.springdata.jpa.security; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.junit.Assert.assertThat; + +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; /** - * Integration test to show the usage of Java 8 date time APIs with Spring Data JPA auditing. + * Integration test to show the usage of Spring Security constructs within Repository query methods. * * @author Oliver Gierke + * @author Thomas Darimont */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SecurityConfiguration.class) @Transactional public class SecurityIntegrationTests { + @Autowired UserRepository userRepository; + @Autowired BusinessObjectRepository businessObjectRepository; + @Autowired SecureBusinessObjectRepository secureBusinessObjectRepository; + + User tom; + User olli; + User admin; + + BusinessObject object1; + BusinessObject object2; + BusinessObject object3; + + @Before + public void setup(){ + + tom = userRepository.save(new User("thomas","darimont","tdarimont@example.org")); + olli = userRepository.save(new User("oliver","gierke","ogierke@example.org")); + admin = userRepository.save(new User("admin","admin","admin@example.org")); + + object1 = businessObjectRepository.save(new BusinessObject("object1", olli)); + object2 = businessObjectRepository.save(new BusinessObject("object2", olli)); + object3 = businessObjectRepository.save(new BusinessObject("object3", tom)); + } + + @Test + public void findBusinessObjectsForCurrentUserShouldReturnOnlyBusinessObjectsWhereCurrentUserIsOwner(){ + + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(tom,"x")); + + List businessObjects = secureBusinessObjectRepository.findBusinessObjectsForCurrentUser(); + + assertThat(businessObjects,hasSize(1)); + assertThat(businessObjects,contains(object3)); + + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(olli,"x")); + + businessObjects = secureBusinessObjectRepository.findBusinessObjectsForCurrentUser(); + + assertThat(businessObjects,hasSize(2)); + assertThat(businessObjects,contains(object1,object2)); + } + + @Test + public void findBusinessObjectsForCurrentUserShouldReturnAllObjectsForAdmin(){ + + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(admin,"x", Collections.singleton(new SimpleGrantedAuthority("ROLE_ADMIN")))); + + List businessObjects = secureBusinessObjectRepository.findBusinessObjectsForCurrentUser(); + + assertThat(businessObjects,hasSize(3)); + assertThat(businessObjects,contains(object1,object2, object3)); + } } diff --git a/jpa/security/src/test/resources/logback.xml b/jpa/security/src/test/resources/logback.xml index b48bf208..e5ed5bce 100644 --- a/jpa/security/src/test/resources/logback.xml +++ b/jpa/security/src/test/resources/logback.xml @@ -15,4 +15,4 @@ - \ No newline at end of file +