Nick Williams (Migrated from SEC-2334) said:
Currently, UrlAuthorizationConfigurer, ExpressionUrlAuthorizationConfigurer, and GlobalMethodSecurityConfiguration provide ways for configuring a custom AccessDecisionManager (either by calling a method, as in the first two cases, or overriding a method, as in the latter case). For all of the standard AccessDecisionManager implementations, you want to construct them with a List of AccessDecisionVoter implementations. Then, some AccessDecisionVoter implementations require either a SecurityExpressionHandler or PreInvocationAuthorizationAdvice to function properly.
So, taking the ExpressionUrlAuthorizationConfigurer case as an example (the UrlAuthorizationConfigurer case is extremely similar), let's take a look at what's involved to simply switch from the AffirmativeBased decision manager to the ConsensusBased decision manager:
@Override
protected void configure(HttpSecurity security) throws Exception {
List<AccessDecisionVoter> decisionVoters = new ArrayList<AccessDecisionVoter>();
WebExpressionVoter expressionVoter = new WebExpressionVoter();
expressionVoter.setExpressionHandler(expressionHandler);
decisionVoters.add(new DefaultWebSecurityExpressionHandler());
security
.authorizeRequests()
.accessDecisionManager(new ConsensusBased(decisionVoters))
...
}
The obvious problem here is that the first four lines of code in this method are duplicated and unnecessary. They are identical to code found in ExpressionUrlAuthorizationConfigurer. Now AbstractInterceptUrlConfigurer defines a package-private, abstract method getDecisionVoters that UrlAuthorizationConfigurer and ExpressionUrlAuthorizationConfigurer implement. How does this configuration method change if we make getDecisionVoters public and take advantage of it?
@Override
protected void configure(HttpSecurity security) throws Exception
{
security
.authorizeRequests()
.accessDecisionManager(new ConsensusBased(
security.authorizeRequests().getDecisionVoters()
))
...
}
This code is much cleaner and takes advantage of the default, standard expression handler and voters while using a different decision manager. For clarity, it might make sense to rename getDecisionVoters to something like createDefaultDecisionVoters, but you get the idea.
Next let's take a look at GlobalMethodSecurityConfiguration. Assume you want to do the same thing with it: just change from the AffirmativeBased to the ConsensusBased decision manager. As of now:
@Configuration
@EnableGlobalMethodSecurity(...)
public static class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {
@Override
protected AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter> decisionVoters = new ArrayList<>();
ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice();
expressionAdvice.setExpressionHandler(getExpressionHandler());
if(prePostEnabled()) {
decisionVoters.add(new PreInvocationAuthorizationAdviceVoter(
expressionAdvice));
}
if(jsr250Enabled()) {
decisionVoters.add(new Jsr250Voter());
}
decisionVoters.add(new RoleVoter());
decisionVoters.add(new AuthenticatedVoter());
return new ConsensusBased(decisionVoters);
}
}
Once again, we've duplicated a lot of code unnecessarily. Additionally, prePostEnabled() and jsr250Enabled() have private access and can't be called from here. Arguably you could eliminate this last problem by only adding the voters that your @EnableGlobalMethodSecurity config requires, but still. It's definitely not clean, and the only way to get it "right" is to have some knowledge of the Spring Security source code. Now let's look at this same change using the (currently non-existent) createDefaultDecisionVoters method:
@Configuration
@EnableGlobalMethodSecurity(...)
public static class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {
@Override
protected AccessDecisionManager accessDecisionManager() {
return new ConsensusBased(createDefaultDecisionVoters());
}
}
WOW. Much cleaner. Adding the createDefaultDecisionVoters() here also provides us with another capability. If you want, you can create the default voters and add just your one custom voter to them, still creating the default decision manager (or a custom decision manager--it's up to you):
@Configuration
@EnableGlobalMethodSecurity(...)
public static class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {
@Override
protected List<AccessDecisionVoter> createDefaultDecisionVoters() {
List<AccessDecisionVoter> list = super.createDefaultDecisionVoters();
list.add(new MyCustomDecisionVoter());
return list;
}
}
Finally, in both of these cases, exposing a getDecisionVoters/createDefaultDecisionVoters method ensures that if any bug fixes are made to the default implementations, or if any new default voters are added, the user gets this update on the next upgrade.
Given these use cases and compelling arguments :-), I propose the following:
Rename AbstractInterceptUrlConfigurer#getDecisionVoters to createDefaultDecisionVoters and make it public.
Create a GlobalMethodSecurityConfiguration#getDecisionVoters method, move the voter creation to it, and make it protected.
It's possible I missed other configuration classes with similar design, in which case I implicitly make analogous proposals for those classes as well.
Comment From: jgoldhammer
I am also struggling with that... This would be nice to see it one of the next versions...
Thanks, JEns
Comment From: mskluev
+1
Spent all day trying to figure out how to add a custom voter to an application already using @EnableGlobalMethodSecurity . The AccessDecisionManager / Voter construct makes it look so easy at first. I felt like I must be doing something wrong having to copy/paste a bunch of code from a spring source code file. At least I found this issue which helped reaffirm I am not crazy.
Thanks!
Comment From: okohub
@rwinch @eleftherias is this issue open to contribute? I would like to add this.
Comment From: jzheaux
Hi, @okohub. I think given https://github.com/spring-projects/spring-security/issues/9290, we'll want to expose this using a custom authorization manager.