I am having issues setting up ACL through Java config in a Spring Boot application. I have created one small project to reproduce the issues.
I have tried a few different approaches. First issue I had was with EhCache, and after I fixed that (I assume I did) I couldn't login any more, and it looks like all the data is gone.
There are 4 classes with different configurations:
ACLConfig1.class
ACLConfig2.class
ACLConfig3.class
ACLConfig4.class
All @PreAuthorize
and @PostAuthorize
annotations are working as expected, except hasPermission
.
Controller holds 4 endpoints: one for User, one for Admin, one Public and the last one which gives me headache @PostAuthorize("hasPermission(returnObject,'administration')")
I am pretty sure that inserts in DB are correct. This class is one of four, the last one that I have tried:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class ACLConfig4 {
@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 DefaultPermissionGrantingStrategy permissionGrantingStrategy() {
ConsoleAuditLogger consoleAuditLogger = new ConsoleAuditLogger();
return new DefaultPermissionGrantingStrategy(consoleAuditLogger);
}
@Bean
public AclAuthorizationStrategy aclAuthorizationStrategy() {
return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_ADMINISTRATOR"));
}
@Bean
public LookupStrategy lookupStrategy() {
return new BasicLookupStrategy(dataSource, aclCache(), aclAuthorizationStrategy(), new ConsoleAuditLogger());
}
@Bean
public JdbcMutableAclService aclService() {
JdbcMutableAclService service = new JdbcMutableAclService(dataSource, lookupStrategy(), aclCache());
return service;
}
@Bean
public DefaultMethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler() {
return new DefaultMethodSecurityExpressionHandler();
}
@Bean
public MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler = defaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new AclPermissionEvaluator(aclService()));
expressionHandler.setPermissionCacheOptimizer(new AclPermissionCacheOptimizer(aclService()));
return expressionHandler;
}
}
What am I missing here? Why I have no data if I use ACLConfig3.class or ACLConfig4.class. Is there any example on how this should be configured programmatically in Spring Boot?
The reason why you have no data was a bit tricky to find out. As soon as you define a MethodSecurityExpressionHandler
bean in your config, there is no data in the database tables. This is because your data.sql
file isn't executed.
Before explaining why data.sql
isn't executed I'd first like to point out that you're not using the file as intended.
data.sql
is executed by spring-boot after hibernate has been initialized and normally only contains DML statements. Your data.sql
contains both DDL (schema) statements and DML (data) statements. This isn't ideal as some of your DDL statements clash with hibernate's hibernate.hbm2ddl.auto
behaviour (note that spring-boot uses 'create-drop' when an embedded DataSource
is being used). You should put your DDL statements in schema.sql
and your DML statements in data.sql
. As you're manually defining all tables you should disable hibernate.hbm2ddl.auto
(by adding spring.jpa.hibernate.ddl-auto=none
to applciation.properties
).
That being said, let's take a look at why data.sql
isn't executed.
The execution of data.sql
is triggered via an ApplicationEvent
that's fired via a BeanPostProcessor
. This BeanPostProcessor
(DataSourceInitializedPublisher
) is created as a part of spring-boot's Hibernate/JPA auto configuration (see org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
, org.springframework.boot.autoconfigure.orm.jpa.DataSourceInitializedPublisher
and org.springframework.boot.autoconfigure.jdbc.DataSourceInitializer
).
Normally the DataSourceInitializedPublisher
is created before the (embedded) DataSource
is created and everything works as expected but by defining a custom MethodSecurityExpressionHandler
the normal bean creation order alters.
As you've configured @EnableGlobalMethodSecurity
, your're automatically importing GlobalMethodSecurityConfiguration
.
spring-security related beans are created early on. As your MethodSecurityExpressionHandler
requires a DataSource
for the ACL stuff and the spring-security related beans require your custom MethodSecurityExpressionHandler
, the DataSource
is created earlier than usual; in fact it's created so early on that spring-boot's DataSourceInitializedPublisher
isn't created yet.
The DataSourceInitializedPublisher
is created later on but as it didn't notice the creation of a DataSource
bean, it also doesn't trigger the execution of data.sql
.
So long story short: the security configuration alters the normal bean creation order which results in data.sql
not being loaded.
I guess that fixing the bean creation order would do the trick, but as I don't now how (without further experimentation) I propose the following solution: manually define your DataSource
and take care of data initialization.
@Configuration
public class DataSourceConfig {
@Bean
public EmbeddedDatabase dataSource() {
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2)
//as your data.sql file contains both DDL & DML you might want to rename it (e.g. init.sql)
.addScript("classpath:/data.sql")
.build();
}
}
As your data.sql file contains all DDL required by your application you can disable hibernate.hbm2ddl.auto
. Add spring.jpa.hibernate.ddl-auto=none
to applciation.properties
.
When defining your own DataSource
spring-boot's DataSourceAutoConfiguration
normally back's out but if you want to be sure you can also exclude it (optional).
@SpringBootConfiguration
@EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class)
@ComponentScan
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
This should fix your 'no data' problem. But in order to get everything working as expected you need to make 2 more modifications.
First of all, you should only define one MethodSecurityExpressionHandler
bean. Currently you're defining 2 MethodSecurityExpressionHandler
beans. Spring-security won't know which one to use and will (silently) use it's own internal MethodSecurityExpressionHandler
instead. See org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration#setMethodSecurityExpressionHandler
.
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MyACLConfig {
//...
@Bean
public MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler securityExpressionHandler = new DefaultMethodSecurityExpressionHandler();
securityExpressionHandler.setPermissionEvaluator(new AclPermissionEvaluator(aclService()));
securityExpressionHandler.setPermissionCacheOptimizer(new AclPermissionCacheOptimizer(aclService()));
return securityExpressionHandler;
}
}
The last thing you need to do is make the getId()
method in Car public.
@Entity
public class Car {
//...
public long getId() {
return id;
}
//...
}
The standard ObjectIdentityRetrievalStrategy
will look for a public method 'getId()' when trying to determine an object's identity during ACL permission evaluation.
(Note that I've based my answer upon ACLConfig4
.)