diff --git a/spring-security-mvc-boot/pom.xml b/spring-security-mvc-boot/pom.xml index 3a8bc1f3aa..6566c0eea6 100644 --- a/spring-security-mvc-boot/pom.xml +++ b/spring-security-mvc-boot/pom.xml @@ -122,6 +122,25 @@ jstl-api ${jstl.version} + + + org.springframework.security + spring-security-acl + + + org.springframework.security + spring-security-config + + + org.springframework + spring-context-support + + + net.sf.ehcache + ehcache-core + 2.6.11 + jar + diff --git a/spring-security-mvc-boot/src/main/java/org/baeldung/acl/config/ACLContext.java b/spring-security-mvc-boot/src/main/java/org/baeldung/acl/config/ACLContext.java new file mode 100644 index 0000000000..63a4ea58ef --- /dev/null +++ b/spring-security-mvc-boot/src/main/java/org/baeldung/acl/config/ACLContext.java @@ -0,0 +1,80 @@ +package org.baeldung.acl.config; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cache.ehcache.EhCacheFactoryBean; +import org.springframework.cache.ehcache.EhCacheManagerFactoryBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.acls.AclPermissionCacheOptimizer; +import org.springframework.security.acls.AclPermissionEvaluator; +import org.springframework.security.acls.domain.AclAuthorizationStrategy; +import org.springframework.security.acls.domain.AclAuthorizationStrategyImpl; +import org.springframework.security.acls.domain.ConsoleAuditLogger; +import org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy; +import org.springframework.security.acls.domain.EhCacheBasedAclCache; +import org.springframework.security.acls.jdbc.BasicLookupStrategy; +import org.springframework.security.acls.jdbc.JdbcMutableAclService; +import org.springframework.security.acls.jdbc.LookupStrategy; +import org.springframework.security.acls.model.PermissionGrantingStrategy; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +@Configuration +@EnableAutoConfiguration +public class ACLContext { + + @Autowired + DataSource dataSource; + + @Bean + public EhCacheBasedAclCache aclCache() { + return new EhCacheBasedAclCache(aclEhCacheFactoryBean().getObject(), permissionGrantingStrategy(), aclAuthorizationStrategy()); + } + + @Bean + public EhCacheFactoryBean aclEhCacheFactoryBean() { + EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean(); + ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject()); + ehCacheFactoryBean.setCacheName("aclCache"); + return ehCacheFactoryBean; + } + + @Bean + public EhCacheManagerFactoryBean aclCacheManager() { + return new EhCacheManagerFactoryBean(); + } + + @Bean + public PermissionGrantingStrategy permissionGrantingStrategy() { + return new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger()); + } + + @Bean + public AclAuthorizationStrategy aclAuthorizationStrategy() { + return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_ADMIN")); + } + + @Bean + public MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler() { + DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + AclPermissionEvaluator permissionEvaluator = new AclPermissionEvaluator(aclService()); + expressionHandler.setPermissionEvaluator(permissionEvaluator); + expressionHandler.setPermissionCacheOptimizer(new AclPermissionCacheOptimizer(aclService())); + return expressionHandler; + } + + @Bean + public LookupStrategy lookupStrategy() { + return new BasicLookupStrategy(dataSource, aclCache(), aclAuthorizationStrategy(), new ConsoleAuditLogger()); + } + + @Bean + public JdbcMutableAclService aclService() { + return new JdbcMutableAclService(dataSource, lookupStrategy(), aclCache()); + } + +} \ No newline at end of file diff --git a/spring-security-mvc-boot/src/main/java/org/baeldung/acl/config/AclMethodSecurityConfiguration.java b/spring-security-mvc-boot/src/main/java/org/baeldung/acl/config/AclMethodSecurityConfiguration.java new file mode 100644 index 0000000000..110c4a6d24 --- /dev/null +++ b/spring-security-mvc-boot/src/main/java/org/baeldung/acl/config/AclMethodSecurityConfiguration.java @@ -0,0 +1,21 @@ +package org.baeldung.acl.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; + +@Configuration +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +public class AclMethodSecurityConfiguration extends GlobalMethodSecurityConfiguration { + + @Autowired + MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler; + + @Override + protected MethodSecurityExpressionHandler createExpressionHandler() { + return defaultMethodSecurityExpressionHandler; + } + +} diff --git a/spring-security-mvc-boot/src/main/java/org/baeldung/acl/config/JPAPersistenceConfig.java b/spring-security-mvc-boot/src/main/java/org/baeldung/acl/config/JPAPersistenceConfig.java new file mode 100644 index 0000000000..9b87efa92c --- /dev/null +++ b/spring-security-mvc-boot/src/main/java/org/baeldung/acl/config/JPAPersistenceConfig.java @@ -0,0 +1,16 @@ +package org.baeldung.acl.config; + +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@Configuration +@EnableTransactionManagement +@EnableJpaRepositories(basePackages = "org.baeldung.acl.persistence.dao") +@PropertySource("classpath:org.baeldung.acl.datasource.properties") +@EntityScan(basePackages={ "org.baeldung.acl.persistence.entity" }) +public class JPAPersistenceConfig { + +} diff --git a/spring-security-mvc-boot/src/main/java/org/baeldung/acl/persistence/dao/NoticeMessageRepository.java b/spring-security-mvc-boot/src/main/java/org/baeldung/acl/persistence/dao/NoticeMessageRepository.java new file mode 100644 index 0000000000..8662c88d6c --- /dev/null +++ b/spring-security-mvc-boot/src/main/java/org/baeldung/acl/persistence/dao/NoticeMessageRepository.java @@ -0,0 +1,24 @@ +package org.baeldung.acl.persistence.dao; + +import java.util.List; + +import org.baeldung.acl.persistence.entity.NoticeMessage; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.access.prepost.PostFilter; +import org.springframework.security.access.prepost.PreAuthorize; + +public interface NoticeMessageRepository extends JpaRepository{ + + @PostFilter("hasPermission(filterObject, 'READ')") + List findAll(); + + @PostAuthorize("hasPermission(returnObject, 'READ')") + NoticeMessage findById(Integer id); + + @SuppressWarnings("unchecked") + @PreAuthorize("hasPermission(#noticeMessage, 'WRITE')") + NoticeMessage save(@Param("noticeMessage")NoticeMessage noticeMessage); + +} diff --git a/spring-security-mvc-boot/src/main/java/org/baeldung/acl/persistence/entity/NoticeMessage.java b/spring-security-mvc-boot/src/main/java/org/baeldung/acl/persistence/entity/NoticeMessage.java new file mode 100644 index 0000000000..23f01a8edb --- /dev/null +++ b/spring-security-mvc-boot/src/main/java/org/baeldung/acl/persistence/entity/NoticeMessage.java @@ -0,0 +1,29 @@ +package org.baeldung.acl.persistence.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name="system_message") +public class NoticeMessage { + + @Id + @Column + private Integer id; + @Column + private String content; + public Integer getId() { + return id; + } + public void setId(Integer id) { + this.id = id; + } + public String getContent() { + return content; + } + public void setContent(String content) { + this.content = content; + } +} \ No newline at end of file diff --git a/spring-security-mvc-boot/src/main/resources/acl-data.sql b/spring-security-mvc-boot/src/main/resources/acl-data.sql new file mode 100644 index 0000000000..6c01eaacc2 --- /dev/null +++ b/spring-security-mvc-boot/src/main/resources/acl-data.sql @@ -0,0 +1,28 @@ +INSERT INTO system_message(id,content) VALUES (1,'First Level Message'); +INSERT INTO system_message(id,content) VALUES (2,'Second Level Message'); +INSERT INTO system_message(id,content) VALUES (3,'Third Level Message'); + +INSERT INTO acl_class (id, class) VALUES +(1, 'org.baeldung.acl.persistence.entity.NoticeMessage'); + +INSERT INTO acl_sid (id, principal, sid) VALUES +(1, 1, 'manager'), +(2, 1, 'hr'), +(3, 1, 'admin'), +(4, 0, 'ROLE_EDITOR'); + +INSERT INTO acl_object_identity (id, object_id_class, object_id_identity, parent_object, owner_sid, entries_inheriting) VALUES +(1, 1, 1, NULL, 3, 0), +(2, 1, 2, NULL, 3, 0), +(3, 1, 3, NULL, 3, 0) +; + +INSERT INTO acl_entry (id, acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) VALUES +(1, 1, 1, 1, 1, 1, 1, 1), +(2, 1, 2, 1, 2, 1, 1, 1), +(3, 1, 3, 4, 1, 1, 1, 1), +(4, 2, 1, 2, 1, 1, 1, 1), +(5, 2, 2, 4, 1, 1, 1, 1), +(6, 3, 1, 4, 1, 1, 1, 1), +(7, 3, 2, 4, 2, 1, 1, 1) +; \ No newline at end of file diff --git a/spring-security-mvc-boot/src/main/resources/acl-schema.sql b/spring-security-mvc-boot/src/main/resources/acl-schema.sql new file mode 100644 index 0000000000..58e9394b2b --- /dev/null +++ b/spring-security-mvc-boot/src/main/resources/acl-schema.sql @@ -0,0 +1,58 @@ +create table system_message (id integer not null, content varchar(255), primary key (id)); + +CREATE TABLE IF NOT EXISTS acl_sid ( + id bigint(20) NOT NULL AUTO_INCREMENT, + principal tinyint(1) NOT NULL, + sid varchar(100) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY unique_uk_1 (sid,principal) +); + +CREATE TABLE IF NOT EXISTS acl_class ( + id bigint(20) NOT NULL AUTO_INCREMENT, + class varchar(255) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY unique_uk_2 (class) +); + +CREATE TABLE IF NOT EXISTS acl_entry ( + id bigint(20) NOT NULL AUTO_INCREMENT, + acl_object_identity bigint(20) NOT NULL, + ace_order int(11) NOT NULL, + sid bigint(20) NOT NULL, + mask int(11) NOT NULL, + granting tinyint(1) NOT NULL, + audit_success tinyint(1) NOT NULL, + audit_failure tinyint(1) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY unique_uk_4 (acl_object_identity,ace_order) +); + +CREATE TABLE IF NOT EXISTS acl_object_identity ( + id bigint(20) NOT NULL AUTO_INCREMENT, + object_id_class bigint(20) NOT NULL, + object_id_identity bigint(20) NOT NULL, + parent_object bigint(20) DEFAULT NULL, + owner_sid bigint(20) DEFAULT NULL, + entries_inheriting tinyint(1) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY unique_uk_3 (object_id_class,object_id_identity) +); + +ALTER TABLE acl_entry +ADD FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity(id); + +ALTER TABLE acl_entry +ADD FOREIGN KEY (sid) REFERENCES acl_sid(id); + +-- +-- Constraints for table acl_object_identity +-- +ALTER TABLE acl_object_identity +ADD FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id); + +ALTER TABLE acl_object_identity +ADD FOREIGN KEY (object_id_class) REFERENCES acl_class (id); + +ALTER TABLE acl_object_identity +ADD FOREIGN KEY (owner_sid) REFERENCES acl_sid (id); \ No newline at end of file diff --git a/spring-security-mvc-boot/src/main/resources/org.baeldung.acl.datasource.properties b/spring-security-mvc-boot/src/main/resources/org.baeldung.acl.datasource.properties new file mode 100644 index 0000000000..739dd3f07c --- /dev/null +++ b/spring-security-mvc-boot/src/main/resources/org.baeldung.acl.datasource.properties @@ -0,0 +1,12 @@ +spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.username=sa +spring.datasource.password= +spring.datasource.driverClassName=org.h2.Driver +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect + +spring.h2.console.path=/myconsole +spring.h2.console.enabled=true +spring.datasource.initialize=true +spring.datasource.schema=classpath:acl-schema.sql +spring.datasource.data=classpath:acl-data.sql \ No newline at end of file diff --git a/spring-security-mvc-boot/src/test/java/org/baeldung/acl/SpringAclTest.java b/spring-security-mvc-boot/src/test/java/org/baeldung/acl/SpringAclTest.java new file mode 100644 index 0000000000..fd9069d9bc --- /dev/null +++ b/spring-security-mvc-boot/src/test/java/org/baeldung/acl/SpringAclTest.java @@ -0,0 +1,119 @@ +package org.baeldung.acl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.util.List; + +import org.baeldung.acl.persistence.dao.NoticeMessageRepository; +import org.baeldung.acl.persistence.entity.NoticeMessage; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.test.context.web.ServletTestExecutionListener; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@TestExecutionListeners(listeners={ServletTestExecutionListener.class, + DependencyInjectionTestExecutionListener.class, + DirtiesContextTestExecutionListener.class, + TransactionalTestExecutionListener.class, + WithSecurityContextTestExecutionListener.class}) +public class SpringAclTest extends AbstractJUnit4SpringContextTests{ + + private static Integer FIRST_MESSAGE_ID = 1; + private static Integer SECOND_MESSAGE_ID = 2; + private static Integer THIRD_MESSAGE_ID = 3; + private static String EDITTED_CONTENT = "EDITED"; + + @Configuration + @ComponentScan("org.baeldung.acl.*") + public static class SpringConfig { + + } + + @Autowired + NoticeMessageRepository repo; + + @Test + @WithMockUser(username="manager") + public void givenUsernameManager_whenFindAllMessage_thenReturnFirstMessage(){ + List details = repo.findAll(); + assertNotNull(details); + assertEquals(1,details.size()); + assertEquals(FIRST_MESSAGE_ID,details.get(0).getId()); + } + + @Test + @WithMockUser(username="manager") + public void givenUsernameManager_whenFindFirstMessageByIdAndUpdateFirstMessageContent_thenOK(){ + NoticeMessage firstMessage = repo.findById(FIRST_MESSAGE_ID); + assertNotNull(firstMessage); + assertEquals(FIRST_MESSAGE_ID,firstMessage.getId()); + + firstMessage.setContent(EDITTED_CONTENT); + repo.save(firstMessage); + + NoticeMessage editedFirstMessage = repo.findById(FIRST_MESSAGE_ID); + assertNotNull(editedFirstMessage); + assertEquals(FIRST_MESSAGE_ID,editedFirstMessage.getId()); + assertEquals(EDITTED_CONTENT,editedFirstMessage.getContent()); + } + + @Test + @WithMockUser(username="hr") + public void givenUsernameHr_whenFindMessageById2_thenOK(){ + NoticeMessage secondMessage = repo.findById(SECOND_MESSAGE_ID); + assertNotNull(secondMessage); + assertEquals(SECOND_MESSAGE_ID,secondMessage.getId()); + } + + @Test(expected=AccessDeniedException.class) + @WithMockUser(username="hr") + public void givenUsernameHr_whenUpdateMessageWithId2_thenFail(){ + NoticeMessage secondMessage = new NoticeMessage(); + secondMessage.setId(SECOND_MESSAGE_ID); + secondMessage.setContent(EDITTED_CONTENT); + repo.save(secondMessage); + } + + @Test + @WithMockUser(roles={"EDITOR"}) + public void givenRoleEditor_whenFindAllMessage_thenReturnThreeMessage(){ + List details = repo.findAll(); + assertNotNull(details); + assertEquals(3,details.size()); + } + + @Test + @WithMockUser(roles={"EDITOR"}) + public void givenRoleEditor_whenUpdateThirdMessage_thenOK(){ + NoticeMessage thirdMessage = new NoticeMessage(); + thirdMessage.setId(THIRD_MESSAGE_ID); + thirdMessage.setContent(EDITTED_CONTENT); + repo.save(thirdMessage); + } + + @Test(expected=AccessDeniedException.class) + @WithMockUser(roles={"EDITOR"}) + public void givenRoleEditor_whenFindFirstMessageByIdAndUpdateFirstMessageContent_thenFail(){ + NoticeMessage firstMessage = repo.findById(FIRST_MESSAGE_ID); + assertNotNull(firstMessage); + assertEquals(FIRST_MESSAGE_ID,firstMessage.getId()); + firstMessage.setContent(EDITTED_CONTENT); + repo.save(firstMessage); + } +} + \ No newline at end of file