浏览代码

SEC-2676: Add SpEL Spring Security Integration

Rob Winch 11 年之前
父节点
当前提交
1f861f512a

+ 1 - 1
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'
     }
 

+ 5 - 0
data/data.gradle

@@ -0,0 +1,5 @@
+dependencies {
+    compile project(':spring-security-core'),
+            "org.springframework.data:spring-data-commons:$springDataCommonsVersion"
+
+}

+ 117 - 0
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;
+
+/**
+ * <p>
+ * By defining this object as a Bean, Spring Security is exposed as SpEL expressions for creating Spring Data
+ * queries.
+ * </p>
+ *
+ * <p>With Java based configuration, we can define the bean using the following:</p>
+ *
+ * <p>For example, if you return a UserDetails that extends the following User object:</p>
+ *
+ * <pre>
+ * @Entity
+ * public class User {
+ *     @GeneratedValue(strategy = GenerationType.AUTO)
+ *     @Id
+ *     private Long id;
+ *
+ *     ...
+ * </pre>
+ *
+ * <p>And you have a Message object that looks like the following:</p>
+ *
+ * <pre>
+ * @Entity
+ * public class Message {
+ *     @Id
+ *     @GeneratedValue(strategy = GenerationType.AUTO)
+ *     private Long id;
+ *
+ *     @OneToOne
+ *     private User to;
+ *
+ *     ...
+ * </pre>
+ *
+ * You can use the following {@code Query} annotation to search for only messages that are to the current user:
+ *
+ * <pre>
+ * @Repository
+ * public interface SecurityMessageRepository extends MessageRepository {
+ *
+ *     @Query("select m from Message m where m.to.id = ?#{ principal?.id }")
+ *     List<Message> findAll();
+ * }
+ * </pre>
+ *
+ * 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();
+    }
+}

+ 75 - 0
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();
+    }
+}

+ 2 - 0
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") {

+ 10 - 0
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"
+
+}

+ 85 - 0
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;
+    }
+}

+ 82 - 0
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;
+    }
+}

+ 26 - 0
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<Message,Long> {
+}

+ 32 - 0
samples/data-jc/src/main/java/samples/data/SecurityMessageRepository.java

@@ -0,0 +1,32 @@
+/*
+ * 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.Query;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * @author Rob Winch
+ */
+@Repository
+public interface SecurityMessageRepository extends MessageRepository {
+    @Override
+    @Query("select m from Message m where m.to.id = ?#{ principal?.id }")
+    List<Message> findAll();
+}

+ 79 - 0
samples/data-jc/src/main/java/samples/data/User.java

@@ -0,0 +1,79 @@
+/*
+ * 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 javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+
+/**
+ * @author Rob Winch
+ */
+@Entity
+public class User {
+    @GeneratedValue(strategy = GenerationType.AUTO)
+    @Id
+    private Long id;
+
+    private String firstName;
+
+    private String lastName;
+
+    private String email;
+
+    private String password;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getFirstName() {
+        return firstName;
+    }
+
+    public void setFirstName(String firstName) {
+        this.firstName = firstName;
+    }
+
+    public String getLastName() {
+        return lastName;
+    }
+
+    public void setLastName(String lastName) {
+        this.lastName = lastName;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+}

+ 73 - 0
samples/data-jc/src/test/java/samples/data/SecurityMessageRepositoryTests.java

@@ -0,0 +1,73 @@
+/*
+ * 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.junit.After;
+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.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import samples.DataConfig;
+
+import java.util.List;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+/**
+ * @author Rob Winch
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(classes = DataConfig.class)
+public class SecurityMessageRepositoryTests {
+    @Autowired
+    SecurityMessageRepository repository;
+
+    User user;
+
+    @Before
+    public void setup() {
+        user = new User();
+        user.setId(0L);
+        List<GrantedAuthority> authorities =
+                AuthorityUtils.createAuthorityList("ROLE_USER");
+        UsernamePasswordAuthenticationToken authentication =
+                new UsernamePasswordAuthenticationToken(user, "notused", authorities);
+        SecurityContextHolder
+                .getContext()
+                .setAuthentication(authentication);
+    }
+
+    @After
+    public void cleanup() {
+        SecurityContextHolder.clearContext();
+    }
+
+    @Test
+    public void findAllOnlyToCurrentUser() {
+        Long expectedId = user.getId();
+        List<Message> messages = repository.findAll();
+        assertThat(messages.size()).isEqualTo(3);
+        for(Message m : messages) {
+            assertThat(m.getTo().getId()).isEqualTo(expectedId);
+        }
+    }
+}

+ 10 - 0
samples/data-jc/src/test/resources/data.sql

@@ -0,0 +1,10 @@
+insert into user(id,email,password,firstName,lastName) values (0,'rob@example.com','password','Rob','Winch');
+insert into user(id,email,password,firstName,lastName) values (1,'luke@example.com','password','Luke','Taylor');
+
+insert into message(id,created,to_id,summary,text) values (100,'2014-07-10 10:00:00',0,'Hello Rob','This message is for Rob');
+insert into message(id,created,to_id,summary,text) values (101,'2014-07-10 14:00:00',0,'How are you Rob?','This message is for Rob');
+insert into message(id,created,to_id,summary,text) values (102,'2014-07-11 22:00:00',0,'Is this secure?','This message is for Rob');
+
+insert into message(id,created,to_id,summary,text) values (110,'2014-07-12 10:00:00',1,'Hello Luke','This message is for Luke');
+insert into message(id,created,to_id,summary,text) values (111,'2014-07-12 10:00:00',1,'Greetings Luke','This message is for Luke');
+insert into message(id,created,to_id,summary,text) values (112,'2014-07-12 10:00:00',1,'Is this secure?','This message is for Luke');

+ 1 - 1
samples/messages-jc/build.gradle

@@ -24,7 +24,7 @@ dependencies {
       compile('org.hibernate:hibernate-entitymanager:3.6.10.Final') {
             exclude group:'javassist', module: 'javassist'
       }
-      compile('org.springframework.data:spring-data-jpa:1.3.4.RELEASE') {
+      compile("org.springframework.data:spring-data-jpa:$springDataJpaVersion") {
             exclude group:'org.aspectj', module:'aspectjrt'
       }
 }

+ 2 - 0
settings.gradle

@@ -1,5 +1,6 @@
 def String[] modules = [
     'core',
+    'data',
     'remoting',
     'web',
     'ldap',
@@ -19,6 +20,7 @@ def String[] samples = [
     'openid-xml',
     'aspectj-xml',
     'aspectj-jc',
+    'data-jc',
     'gae-xml',
     'dms-xml',
     'preauth-xml',