diff --git a/config/config.gradle b/config/config.gradle index e8ecb0f285..4cc33f79b7 100644 --- a/config/config.gradle +++ b/config/config.gradle @@ -54,7 +54,7 @@ dependencies { testCompile('org.openid4java:openid4java-nodeps:0.9.6') { exclude group: 'com.google.code.guice', module: 'guice' } - testCompile('org.springframework.data:spring-data-jpa:1.4.1.RELEASE') { + testCompile("org.springframework.data:spring-data-jpa:$springDataJpaVersion") { exclude group: 'org.aspectj', module: 'aspectjrt' } diff --git a/data/data.gradle b/data/data.gradle new file mode 100644 index 0000000000..0d27da17e3 --- /dev/null +++ b/data/data.gradle @@ -0,0 +1,5 @@ +dependencies { + compile project(':spring-security-core'), + "org.springframework.data:spring-data-commons:$springDataCommonsVersion" + +} \ No newline at end of file diff --git a/data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java b/data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java new file mode 100644 index 0000000000..3af6556aa9 --- /dev/null +++ b/data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-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 org.springframework.security.data.repository.query; + +import org.springframework.data.repository.query.spi.EvaluationContextExtension; +import org.springframework.data.repository.query.spi.EvaluationContextExtensionSupport; +import org.springframework.data.repository.query.spi.Function; +import org.springframework.security.access.expression.SecurityExpressionRoot; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.Map; + +/** + *
+ * By defining this object as a Bean, Spring Security is exposed as SpEL expressions for creating Spring Data + * queries. + *
+ * + *With Java based configuration, we can define the bean using the following:
+ * + *For example, if you return a UserDetails that extends the following User object:
+ * + *
+ * @Entity
+ * public class User {
+ * @GeneratedValue(strategy = GenerationType.AUTO)
+ * @Id
+ * private Long id;
+ *
+ * ...
+ *
+ *
+ * And you have a Message object that looks like the following:
+ * + *
+ * @Entity
+ * public class Message {
+ * @Id
+ * @GeneratedValue(strategy = GenerationType.AUTO)
+ * private Long id;
+ *
+ * @OneToOne
+ * private User to;
+ *
+ * ...
+ *
+ *
+ * You can use the following {@code Query} annotation to search for only messages that are to the current user:
+ *
+ *
+ * @Repository
+ * public interface SecurityMessageRepository extends MessageRepository {
+ *
+ * @Query("select m from Message m where m.to.id = ?#{ principal?.id }")
+ * List findAll();
+ * }
+ *
+ *
+ * This works because the principal in this instance is a User which has an id field on it.
+ *
+ * @since 4.0
+ * @author Rob Winch
+ */
+public class SecurityEvaluationContextExtension extends EvaluationContextExtensionSupport {
+ private Authentication authentication;
+
+ /**
+ * Creates a new instance that uses the current {@link Authentication} found on the
+ * {@link org.springframework.security.core.context.SecurityContextHolder}.
+ */
+ public SecurityEvaluationContextExtension() {
+ }
+
+ /**
+ * Creates a new instance that always uses the same {@link Authentication} object.
+ *
+ * @param authentication the {@link Authentication} to use
+ */
+ public SecurityEvaluationContextExtension(Authentication authentication) {
+ this.authentication = authentication;
+ }
+
+ @Override
+ public String getExtensionId() {
+ return "security";
+ }
+
+ @Override
+ public Object getRootObject() {
+ Authentication authentication = getAuthentication();
+ return new SecurityExpressionRoot(authentication) {};
+ }
+
+ private Authentication getAuthentication() {
+ if(authentication != null) {
+ return authentication;
+ }
+
+ SecurityContext context = SecurityContextHolder.getContext();
+ return context.getAuthentication();
+ }
+}
\ No newline at end of file
diff --git a/data/src/test/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtensionTests.java b/data/src/test/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtensionTests.java
new file mode 100644
index 0000000000..6084626277
--- /dev/null
+++ b/data/src/test/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtensionTests.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2002-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 org.springframework.security.data.repository.query;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.security.access.expression.SecurityExpressionRoot;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class SecurityEvaluationContextExtensionTests {
+ SecurityEvaluationContextExtension securityExtension;
+
+ @Before
+ public void setup() {
+ securityExtension = new SecurityEvaluationContextExtension();
+ }
+
+ @After
+ public void cleanup() {
+ SecurityContextHolder.clearContext();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void getRootObjectSecurityContextHolderAuthenticationNull() {
+ getRoot().getAuthentication();
+ }
+
+ @Test
+ public void getRootObjectSecurityContextHolderAuthentication() {
+ TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ assertThat(getRoot().getAuthentication()).isSameAs(authentication);
+ }
+
+ @Test
+ public void getRootObjectExplicitAuthenticationOverridesSecurityContextHolder() {
+ TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT");
+ securityExtension = new SecurityEvaluationContextExtension(explicit);
+
+ TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ assertThat(getRoot().getAuthentication()).isSameAs(explicit);
+ }
+
+ @Test
+ public void getRootObjectExplicitAuthentication() {
+ TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT");
+ securityExtension = new SecurityEvaluationContextExtension(explicit);
+
+ assertThat(getRoot().getAuthentication()).isSameAs(explicit);
+ }
+
+ private SecurityExpressionRoot getRoot() {
+ return (SecurityExpressionRoot) securityExtension.getRootObject();
+ }
+}
\ No newline at end of file
diff --git a/gradle/javaprojects.gradle b/gradle/javaprojects.gradle
index d0a0a35d3b..52b43670c1 100644
--- a/gradle/javaprojects.gradle
+++ b/gradle/javaprojects.gradle
@@ -28,6 +28,8 @@ ext.groovyVersion = '2.0.5'
ext.spockVersion = '0.7-groovy-2.0'
ext.gebVersion = '0.9.0'
ext.thymeleafVersion = '2.1.3.RELEASE'
+ext.springDataJpaVersion = '1.7.0.M1'
+ext.springDataCommonsVersion = '1.9.0.M1'
ext.spockDependencies = [
dependencies.create("org.spockframework:spock-spring:$spockVersion") {
diff --git a/samples/data-jc/build.gradle b/samples/data-jc/build.gradle
new file mode 100644
index 0000000000..4f743199c6
--- /dev/null
+++ b/samples/data-jc/build.gradle
@@ -0,0 +1,10 @@
+dependencies {
+ compile project(':spring-security-data'),
+ project(':spring-security-config'),
+ "org.springframework.data:spring-data-jpa:$springDataJpaVersion",
+ "org.hibernate.javax.persistence:hibernate-jpa-2.0-api:1.0.0.Final",
+ "org.hsqldb:hsqldb:2.2.8",
+ "javax.validation:validation-api:1.0.0.GA",
+ "org.hibernate:hibernate-validator:4.2.0.Final"
+
+}
\ No newline at end of file
diff --git a/samples/data-jc/src/main/java/samples/DataConfig.java b/samples/data-jc/src/main/java/samples/DataConfig.java
new file mode 100644
index 0000000000..23c488ad61
--- /dev/null
+++ b/samples/data-jc/src/main/java/samples/DataConfig.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2002-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 samples;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.DependsOn;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
+import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.orm.jpa.vendor.Database;
+import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
+import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension;
+import org.springframework.transaction.PlatformTransactionManager;
+import samples.data.Message;
+
+import javax.sql.DataSource;
+
+/**
+ * @author Rob Winch
+ */
+@Configuration
+@ComponentScan
+@EnableJpaRepositories
+public class DataConfig {
+
+ @Bean
+ public SecurityEvaluationContextExtension expressionEvaluationContextProvider() {
+ return new SecurityEvaluationContextExtension();
+ }
+
+ @Bean
+ public DataSource dataSource() {
+ EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
+ return builder.setType(EmbeddedDatabaseType.HSQL).build();
+ }
+
+ @Bean
+ public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
+ HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
+ vendorAdapter.setDatabase(Database.HSQL);
+ vendorAdapter.setGenerateDdl(true);
+
+ LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
+ factory.setJpaVendorAdapter(vendorAdapter);
+ factory.setPackagesToScan(Message.class.getPackage().getName());
+ factory.setDataSource(dataSource());
+
+ return factory;
+ }
+
+ @Bean
+ @DependsOn("entityManagerFactory")
+ public ResourceDatabasePopulator initDatabase(DataSource dataSource) throws Exception {
+ ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
+ populator.addScript(new ClassPathResource("data.sql"));
+ populator.populate(dataSource.getConnection());
+ return populator;
+ }
+
+ @Bean
+ public PlatformTransactionManager transactionManager() {
+ JpaTransactionManager txManager = new JpaTransactionManager();
+ txManager.setEntityManagerFactory(entityManagerFactory().getObject());
+ return txManager;
+ }
+}
diff --git a/samples/data-jc/src/main/java/samples/data/Message.java b/samples/data-jc/src/main/java/samples/data/Message.java
new file mode 100644
index 0000000000..05a8be43aa
--- /dev/null
+++ b/samples/data-jc/src/main/java/samples/data/Message.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2002-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 samples.data;
+
+
+import java.util.Calendar;
+
+import javax.persistence.*;
+
+import org.hibernate.validator.constraints.NotEmpty;
+
+@Entity
+public class Message {
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private Long id;
+
+ @NotEmpty(message = "Message is required.")
+ private String text;
+
+ @NotEmpty(message = "Summary is required.")
+ private String summary;
+
+ @Version
+ private Calendar created = Calendar.getInstance();
+
+ @OneToOne
+ private User to;
+
+ public User getTo() {
+ return to;
+ }
+
+ public void setTo(User to) {
+ this.to = to;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Calendar getCreated() {
+ return created;
+ }
+
+ public void setCreated(Calendar created) {
+ this.created = created;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+
+ public String getSummary() {
+ return summary;
+ }
+
+ public void setSummary(String summary) {
+ this.summary = summary;
+ }
+}
diff --git a/samples/data-jc/src/main/java/samples/data/MessageRepository.java b/samples/data-jc/src/main/java/samples/data/MessageRepository.java
new file mode 100644
index 0000000000..878279cda0
--- /dev/null
+++ b/samples/data-jc/src/main/java/samples/data/MessageRepository.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2002-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 samples.data;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * @author Rob Winch
+ */
+@Repository
+public interface MessageRepository extends JpaRepository