I've run into some problems trying to implemented a role hierarchy in Spring Security with JavaConfig rather than XML. Is there any way to implement a role hierarchy with the #Secured annotation rather than HttpSecurity antMatchers? I cannot seem to add the role hierarchy to HttpSecurity without providing the proper String patterns, though I'd like to be able to make access decisions exclusively using #Secured.
java.lang.IllegalStateException: At least one mapping is required (i.e. authorizeRequests().anyRequest.authenticated())
#Bean
public RoleHierarchyImpl roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
return roleHierarchy;
}
private SecurityExpressionHandler<FilterInvocation> webExpressionHandler() {
DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
defaultWebSecurityExpressionHandler.setRoleHierarchy(roleHierarchy());
return defaultWebSecurityExpressionHandler;
}
public void configure(HttpSecurity http){
http.authorizeRequests().expressionHandler(webExpressionHandler()).and().//other stuff
}
Any help on this is greatly appreciated.
I also wanted to have a role hierarchy using #Secured annotation. Found it really tricky but eventually have a following solution (groovy code).
Define your voters and decision manager in #Configuration class:
#Bean
public static RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl()
roleHierarchy.setHierarchy("""\
$SUPER_ADMIN > $ORGANIZATION_ADMIN
$ORGANIZATION_ADMIN > $DOCTOR
$DOCTOR > $NURSE
$NURSE > $PATIENT
$PATIENT > $USER""".stripIndent())
roleHierarchy
}
#Bean
public static RoleHierarchyVoter roleVoter() {
new RoleHierarchyVoter(roleHierarchy())
}
#Bean
public AffirmativeBased accessDecisionManager() {
List<AccessDecisionVoter> decisionVoters = new ArrayList<>();
decisionVoters.add(webExpressionVoter());
decisionVoters.add(roleVoter());
new AffirmativeBased(decisionVoters);
}
private WebExpressionVoter webExpressionVoter() {
WebExpressionVoter webExpressionVoter = new WebExpressionVoter()
webExpressionVoter.setExpressionHandler(expressionHandler())
webExpressionVoter
}
#Bean
public DefaultWebSecurityExpressionHandler expressionHandler(){
DefaultWebSecurityExpressionHandler expressionHandler = new DefaultWebSecurityExpressionHandler();
expressionHandler.setRoleHierarchy(roleHierarchy());
return expressionHandler;
}
then in security configuration add the decision manager:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.and()
.authorizeRequests()
.accessDecisionManager(accessDecisionManager())
.antMatchers("/auth").permitAll()
...
}
Then you also have to overwrite the GlobalMethodSecurityConfiguration to also use your RoleHierarchyVoter:
#Configuration
#EnableGlobalMethodSecurity(securedEnabled = true)
class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
new DefaultMethodSecurityExpressionHandler(roleHierarchy: SecurityConfiguration.roleHierarchy())
}
#Override
protected AccessDecisionManager accessDecisionManager() {
AffirmativeBased manager = super.accessDecisionManager() as AffirmativeBased
manager.decisionVoters.clear()
manager.decisionVoters << SecurityConfiguration.roleVoter()
manager
}
}
I'm removing other voters and just adding my RoleHierarchyVoter in AccessDecisionManager but you can keep others if you need. In my case I'm only using #Secured annotation thus don't need others. This is the part that is not mentioned anywhere to make it working with #Secured annotation.
Another solution is just to create a RoleHierarchy bean with hierarchy configuration and inject it into your custom authentication filter where you would authenticate the user and pass authorities to UsernamePasswordAuthenticationToken by calling:
roleHierarchy.getReachableGrantedAuthorities(authorityFromUserDetails)
This second solution is a bit tricky if you are not using any custom authorization but in my case it was even simpler.
Related
I'm building an application using Spring Data Rest, Spring Boot and Spring Security. I need to use #Secured annotations on methods and I've configured Spring Security in the following way:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// #formatter:off
#Override
protected void configure(final HttpSecurity http) throws Exception {
http
.securityContext().securityContextRepository(securityContextRepository())
.and()
.exceptionHandling()
.accessDeniedPage(RestPath.Errors.ROOT + RestPath.Errors.FORBIDDEN)
.and()
.csrf().disable();
}
// #formatter:on
#Bean
public SecurityContextRepository securityContextRepository() {
return new ApiUserSecurityContextRepository();
}
#Bean
public UserDetailsService userDetailsService() {
return new ApiUserDetailsService();
}
#Bean
public AuthenticationManager authenticationManager() throws Exception {
return new ProviderManager(Collections.singletonList(authenticationProvider()));
}
#Bean
public AuthenticationProvider authenticationProvider() throws Exception {
final DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService());
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
This type of configuration works well for regular MVC controllers and returns 403 when I try to access them. For example, the following controller security works:
#ResponseBody
#RequestMapping(value = RestPath.Configs.SLASH_TEST, method = RequestMethod.GET, produces = MediaTypes.HAL_JSON_VALUE)
#Secured({"ROLE_USER"})
public ResponseEntity test(#RequestParam(value = RestParam.DB_TEST, required = false) final boolean dbTest) throws ApplicationAvailabilityException {
final AppTestData appTestData = configService.testAppAvailability(dbTest);
return ResponseEntity.ok(projectionFactory.createProjection(AppTestProjection.class, appTestData));
}
However, when I try to use #Secured annotation over a rest repository - it does NOT, e.g.:
#RepositoryRestResource(collectionResourceRel = Shop.COLLECTION_NAME, path = RestResourceRel.SHOPS, excerptProjection = StandardShopProjection.class)
#Secured({"ROLE_USER"})
public interface RestShopRepository extends MongoRepository<Shop, String> {
#Secured({"ROLE_ADMIN"})
#Override
Shop findOne(String s);
}
ApiUserSecurityContextRepository is getting called for both of the methods, but only a custom MVC controller is get to the end of chain and I can check that it accesses vote() method in RoleVoter class for granting access.
As an example, I've checked Spring Data Rest + Spring Security sample, so #Secured or #PreAuthorize annotations should work with Spring Data Rest. Any ideas why they don't work?
Finally resolved the issue. The problem was in the following, I had another ShopRepository in different application module, which was not annotated with #RepositoryRestResource and it was the one which was used when accessing it using REST.
The following line of configuration in custom RepositoryRestConfigurerAdapter fixed the exploration of repositories which need to be exposed, so only annotated ones are exposed now:
config.setRepositoryDetectionStrategy(RepositoryDetectionStrategy.RepositoryDetectionStrategies.ANNOTATED);
After that I could not access the resource at all using REST, so I've figured out that it is not visible to Spring. I just had to enable Mongo repositories on API level with annotation #EnableMongoRepositories.
I have done a lot of Research and to me everything looks right... but I cannot get this to work! Anyone has any idea?
No matter what I do, the relevant mapping remains public to anyone (anonymous or logged in, no matter what Role they have).
Ideally I would like to have ALL requests to be Public, except those which are annotated by #Secured() - obviously only the users with the specific roles would be allowed access to these mappings.
Is that possible?
FYI as a workaround I currently built a method "hasRole(String role)" which checks the role of the logged-in user, and throws a NotAuthorizedException (custom made) if the method returns false.
UserDetails
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> grantedAuthorities = null;
System.out.print("Account role... ");
System.out.println(account.getRole());
if (account.getRole().equals("USER")) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
grantedAuthorities = Arrays.asList(grantedAuthority);
}
if (account.getRole().equals("ADMIN")) {
GrantedAuthority grantedAuthorityUser = new SimpleGrantedAuthority("ROLE_USER");
GrantedAuthority grantedAuthorityAdmin = new SimpleGrantedAuthority("ROLE_ADMIN");
grantedAuthorities = Arrays.asList(grantedAuthorityUser, grantedAuthorityAdmin);
}
return grantedAuthorities;
}
SecurityConfig
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AuthFailure authFailure;
#Autowired
private AuthSuccess authSuccess;
#Autowired
private EntryPointUnauthorizedHandler unauthorizedHandler;
#Autowired
private UserDetailsServiceImpl userDetailsService;
/*#Autowired
public void configAuthBuilder(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userDetailsService);
}*/
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Autowired
#Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userDetailsService);
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().csrfTokenRepository(csrfTokenRepository())
.and().exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
.and().formLogin().loginPage("/login").successHandler(authSuccess).failureHandler(authFailure)
//.and().authorizeRequests().antMatchers("/rest/**").authenticated()
//.and().authorizeRequests().antMatchers("/**").permitAll()
.and().addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);;
}
AccountController
#Secured("ROLE_USER")
#RequestMapping(method = RequestMethod.GET)
public List<Account> getAllAccounts(#RequestParam(value = "mail", required = false) String mail) {
Thanks!
You can make use of Controller scoped Security with Spring HttpSecurity. Try add this to your configure Method:
.antMatchers("rest/accounts*").hasRole("ADMIN")
And if you wish ANY Request to be public (really?):
.anyRequest().permitAll()
You can additionally secure your Methodinvocation for Example in your UserDetailsService when you access it from anywhere:
#Secured("ROLE_USER")
public getAllAccounts(...){...}
Only then you have to annotate your SecurityConfig with:
#EnableGlobalMethodSecurity(securedEnabled = true)
In practice we recommend that you use method security at your service
layer, to control access to your application, and do not rely entirely
on the use of security constraints defined at the web-application
level. URLs change and it is difficult to take account of all the
possible URLs that an application might support and how requests might
be manipulated. You should try and restrict yourself to using a few
simple ant paths which are simple to understand. Always try to use
a"deny-by-default" approach where you have a catch-all wildcard ( / or
) defined last and denying access. Security defined at the service
layer is much more robust and harder to bypass, so you should always
take advantage of Spring Security’s method security options.
see: http://docs.spring.io/autorepo/docs/spring-security/4.0.0.CI-SNAPSHOT/reference/htmlsingle/#request-matching
Here, I would like to add something based on the above right answer from sven.kwiotek. If in the ROLE table you still want to use "USER", "ADMIN"... the solution is also easy:
When fetch the role from database, do not forget to add "ROLE_" prefix manully, for example,
List<GrantedAuthority> authorities = user.getRoles().stream().map(role ->
new SimpleGrantedAuthority("ROLE_" + role.getRole()))
.collect(Collectors.toList());
and then you could use annotation #Secured("ROLE_USER") in the controller method with safety.
The reason is that in the org.springframework.security.access.vote.RoleVoter class all roles should start with ROLE_ prefix.
Using Spring Security 3.2.5 and Spring 4.1.2, 100% Java config
Our webapp has global method security enabled and service methods annotated with #PreAuthorize - everything is working as expected. I'm trying to add a role hierarchy and having no success at all. Here's the hierarchy I'm trying to achieve:
ROLE_ADMIN can access all methods that ROLE_USER can access.
ROLE_USER can access all methods that ROLE_DEFAULT can access.
Despite my best efforts, a user with ROLE_ADMIN receives a 403 when doing something that results in a call to a method annotated with #PreAuthorized("hasAuthority('ROLE_DEFAULT')")
Here's the relevant configuration code:
AppInitializer
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer
{
#Override
protected Class<?>[] getRootConfigClasses()
{
return new Class[]
{
AppConfig.class, SecurityConfig.class
};
}
#Override
protected Class<?>[] getServletConfigClasses()
{
return new Class[]
{
MvcConfig.class
};
}
// other methods not shown for brevity
}
AppConfig.java
#Configuration
#ComponentScan(basePackages={"myapp.config.profile", "myapp.dao", "myapp.service", "myapp.security"})
public class AppConfig
{
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth,
AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> detailSvc) throws Exception
{
PreAuthenticatedAuthenticationProvider authProvider = new PreAuthenticatedAuthenticationProvider();
authProvider.setPreAuthenticatedUserDetailsService(detailSvc);
auth.authenticationProvider(authProvider);
}
// other methods not shown for brevity
}
SecurityConfig.java
#Configuration
#EnableWebMvcSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
#Override
protected void configure(HttpSecurity http) throws Exception
{
PKIAuthenticationFilter pkiFilter = new PKIAuthenticationFilter();
pkiFilter.setAuthenticationManager(authenticationManagerBean());
http.authorizeRequests()
.antMatchers("/app/**").fullyAuthenticated()
.and()
.anonymous().disable()
.jee().disable()
.formLogin().disable()
.csrf().disable()
.x509().disable()
.addFilter(pkiFilter)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Override
public void configure(WebSecurity web) throws Exception
{
// ignore everything but /app/*
web.ignoring().regexMatchers("^(?!/app/).*");
}
}
MvcConfig.java
#Configuration
#EnableWebMvc
#ComponentScan({"myapp.controller"})
public class MvcConfig extends WebMvcConfigurerAdapter
{
// resource handlers, content negotiation, message converters configured here
}
In the same package as SecurityConfig (so it is thus part of the AppConfig component scan) I had this class:
GlobalMethodSecurityConfig.java
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled=true)
public class GlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration
{
#Bean
public RoleHierarchy roleHierarchy()
{
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER > ROLE_DEFAULT");
return roleHierarchy;
}
#Bean
public RoleVoter roleVoter()
{
return new RoleHierarchyVoter(roleHierarchy);
}
#Bean
#Override
protected AccessDecisionManager accessDecisionManager()
{
return new AffirmativeBased(Arrays.asList(roleVoter()));
}
// The method below was added in an attempt to get things working but it is never called
#Override
protected MethodSecurityExpressionHandler createExpressionHandler()
{
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy());
return handler;
}
}
In another attempt I made AppConfig extend GlobalMethodSecurityConfiguration but a user with ROLE_ADMIN cannot call a method requiring ROLE_DEFAULT access.
I'm sure I've misconfigured something somewhere but I can't figure out where I've gone wrong despite reading everything I can find on configuring global method security with a role hierarchy. It appears this would be trivial using XML configuration but the Java config solution eludes me.
I'd override GlobalMethodSecurityConfiguration#accessDecisionManager method. You can see source code that RoleVoter uses.
Here is my suggested overridden source code.
#Override
protected AccessDecisionManager accessDecisionManager() {
var roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_SUPER > ROLE_ADMIN");
var expressionHandler = (DefaultMethodSecurityExpressionHandler) getExpressionHandler();
expressionHandler.setRoleHierarchy(roleHierarchy);
var expressionAdvice = new ExpressionBasedPreInvocationAdvice();
expressionAdvice.setExpressionHandler(expressionHandler);
return new AffirmativeBased(List.of(
new RoleHierarchyVoter(roleHierarchy),
new PreInvocationAuthorizationAdviceVoter(expressionAdvice),
new AuthenticatedVoter(),
new Jsr250Voter()
));
}
Since this question keeps getting views I thought I'd post a follow-up to it. The problem appears to be with the line
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER > ROLE_DEFAULT");
I don't remember why I wrote the hierarchy like that but it's not correct. The API for that method handles the same situation thusly:
Role hierarchy: ROLE_A > ROLE_B and ROLE_B > ROLE_C.
Directly assigned authority: ROLE_A.
Reachable authorities: ROLE_A, ROLE_B, ROLE_C.
Eventually it became clear that a hierarchical model didn't fit our roles so we instead implemented a finer-grained set of authorities mapped to roles, as mentioned in the Spring Security Reference:
For more complex requirements you may wish to define a logical mapping between the specific access-rights your application requires and the roles that are assigned to users, translating between the two when loading the user information.
I am using Spring Security (3.2.5.RELEASE). I try to achieve below scenario:
I have two kind of users in my application: regular user and admin
I want to use #Secured annotation on controller methods (annotated with #RequestMapping)
Methods which are not annotated with #Secured I want to be accessible for all (even anonymous users).
Methods which are annotated with #Secured are permited for regular users if they have specific role passed to #Secured annotation. These methods should be also always permited for admin users but I don't want to put ROLE_ADMIN on every time when I use #Secured annotation.
This is my HttpSession configuration:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**")
.permitAll()
[...]
}
I can annotate my controller methods with #Secured annotation and it works. The only problem is how I can add url-intercept matcher for permitting all users which has role ROLE_ADMIN before access rules collected from #Secured annotatated methods. For now it looks like #Secured methods are first in filter chain and rules added to HttpSession in code above are last. How can I add rule that will be first (for ROLE_ADMIN permision) and last (for all methods not annotated with #Secured permision), and all rules from #Secured annotations will be wrapped by these two rules? To be more clear, I want to achieve something like this (in chain):
1) allow all for users with ROLE_ADMIN
2) all rules from #Secured
3) allow methods not annotated with #Secured for all
Ok, I've achieved that but not exactly like in question. This is what I've done:
Create own AccessDecisionVoter which will always return ACCESS_GRANTED for user which is admin.
Override default AccessDecisionManagers creation: both!!! One for url intercepting and one for method intercepting.
This is my AdminPermitVoter
public class AdminPermitVoter implements AccessDecisionVoter<Object> {
#Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
#Override
public boolean supports(Class<?> clazz) {
return true;
}
#Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
if(isAdmin(extractAuthorities(authentication))) {
return ACCESS_GRANTED;
}
return ACCESS_ABSTAIN;
}
Collection<? extends GrantedAuthority> extractAuthorities(Authentication authentication) {
return authentication.getAuthorities();
}
private boolean isAdmin(Collection<? extends GrantedAuthority> authorities) {
for(GrantedAuthority authority : authorities) {
if(equalsIgnoreCase(ADMIN_ROLE_NAME, authority.getAuthority())) {
return true;
}
}
return false;
}
}
This is creation of default url interecption access decision manager:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.accessDecisionManager(accessDecisionManager())
.anyRequest()
.permitAll()
[...other configs...]
}
#Bean(name = "accessDecisionManager")
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter> voters = new ArrayList<>();
voters.add(new AdminPermitVoter());
voters.add(new WebExpressionVoter());
voters.add(new RoleVoter());
voters.add(new AuthenticatedVoter());
return new AffirmativeBased(voters);
}
This is creation of default method interception access decision manager:
#Configuration
#EnableGlobalMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
#SuppressWarnings("rawtypes")
#Override
protected AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter> voters = new ArrayList<>();
voters.add(new AdminPermitVoter());
voters.add(new RoleVoter());
voters.add(new AuthenticatedVoter());
return new AffirmativeBased(voters);
}
}
I have a problem setting Spring Security up.
So to start with, I have a Configuration class, something like this:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity
#ComponentScan("com.boardviewer")
public class BoardviewerConfiguration extends WebSecurityConfigurerAdapter {
#Inject
private BoardviewerSecurityService boardviewerSecurityService;
#Bean
public InternalResourceViewResolver internalResourceViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/pages/");
resolver.setSuffix(".jsp");
return resolver;
}
#Bean /* The "${props} can now be parsed before runtime with this bean declaration */
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
/* Spring Sec */
#Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider dao = new DaoAuthenticationProvider();
dao.setUserDetailsService(boardviewerSecurityService);
return dao;
}
#Bean
public ProviderManager providerManager() {
List<AuthenticationProvider> list = new ArrayList<AuthenticationProvider>();
list.add(daoAuthenticationProvider());
return new ProviderManager(list);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
auth.authenticationProvider(daoAuthenticationProvider());
auth.userDetailsService(boardviewerSecurityService);
}
}
Basically some basic WebSecurity configs...
I'm not getting ANY errors, but I can browse around the site normally without any restrictions.
For instance, I have a controller with the #PreAuthorize annotation, and I get straight through it.
I'm running Spring Security 3.2.0 RC2 to be able to get an annotation configuration going... But so far no luck.
Is there any additional config in the web.xml needed? Or am I missing something?
Anyone got an example of a working Spring Security annotation config?
Also, I'm using hibernate to fetch User accounts etc, and my boardviewerSecurityService looks like this:
#Service
public class BoardviewerSecurityService implements UserDetailsService {
#Inject
private UserDAO userDAO;
#Inject
private BoardviewerTransformer transformer;
#Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User u = userDAO.getByUsername(s);
if(u == null) {
throw new UsernameNotFoundException("Couldn't find a user with that username");
} else {
return transformer.userToSpringUser(u);
}
}
}
And the transformer simply remaps the entity to a Spring UserDetails User object (org.springframework.security.core.userdetails.User)
Am I missing something? (A part from setting up the login page and url interceptors? I thought I won't need those since I only want to control access on class / method level)
Would appreciate any help!
Regards