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);
}
}
Related
I have a standard Spring MVC web application, and I want to add a custom annotation that takes a parameter and the HttpRequest.
Now I know that annotation parameters are resolved at compile time, but how does Spring security get access to the session and user and stuff with #PreAuth and stuff...
I am specifically looking to get the HttpRequest.
Ideas how to work around this?
U should implement PermissionEvaluator.
public class CustomPermissionEvaluator implements PermissionEvaluator {
public boolean hasPermission(Authentication authentication, Object target,
Object permission) {
return // you logic;
}
public boolean hasPermission(Authentication authentication, Serializable targetId,String targetType, Object permission) {
return // you logic;
}
}
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator());
return expressionHandler;
}
#PreAuthorize("hasPermission(#foo, 'YOU_CUSTOM_PARAM')")
#PostMapping(value = "/pay")
public Foo create(Foo foo) {
return foo;
}
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.
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'm using Dynamic datasource routing as indicated in this blog post:
http://spring.io/blog/2007/01/23/dynamic-datasource-routing/
This works fine, but when I combine it with spring-data-rest and browsing of my generated repositories I (rightfully) get an exception that my lookup-key is not defined (I do not set a default).
How and where can I hook into the Spring data rest request handling to set the lookup-key based on 'x' (user authorizations, path prefix, or other), before any connection is made to the database?
Code-wise my datasource configuration just mostly matches the blogpost at the top, with some basic entity classes, generated repositories and Spring Boot to wrap everything together. If need I could post some code, but there's nothing much to see there.
My first idea is to leverage Spring Security's authentication object to set current datasource based on authorities attached to the authentication.
Of course, you can put the lookup key in a custom UserDetails object or even a custom Authentication object, too. For sake of brevity I`ll concentrate on a solution based on authorities.
This solution requires a valid authentication object (anonymous user can have a valid authentication, too). Depending on your Spring Security configuration changing authority/datasource can be accomplished on a per request or session basis.
My second idea is to work with a javax.servlet.Filter to set lookup key in a thread local variable before Spring Data Rest kicks in. This solution is framework independent and can be used on a per request or session basis.
Datasource routing with Spring Security
Use SecurityContextHolder to access current authentication's authorities. Based on the authorities decide which datasource to use.
Just as your code I'm not setting a defaultTargetDataSource on my AbstractRoutingDataSource.
public class CustomRoutingDataSource extends AbstractRoutingDataSource {
#Override
protected Object determineCurrentLookupKey() {
Set<String> authorities = getAuthoritiesOfCurrentUser();
if(authorities.contains("ROLE_TENANT1")) {
return "TENANT1";
}
return "TENANT2";
}
private Set<String> getAuthoritiesOfCurrentUser() {
if(SecurityContextHolder.getContext().getAuthentication() == null) {
return Collections.emptySet();
}
Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
return AuthorityUtils.authorityListToSet(authorities);
}
}
In your code you must replace the in memory UserDetailsService (inMemoryAuthentication) with a UserDetailsService that serves your need.
It shows you that there are two different users with different roles TENANT1 and TENANT2 used for the datasource routing.
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user1").password("user1").roles("USER", "TENANT1")
.and()
.withUser("user2").password("user2").roles("USER", "TENANT2");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/**").hasRole("USER")
.and()
.httpBasic()
.and().csrf().disable();
}
}
Here is a complete example: https://github.com/ksokol/spring-sandbox/tree/sdr-routing-datasource-spring-security/spring-data
Datasource routing with javax.servlet.Filter
Create a new filter class and add it to your web.xml or register it with the AbstractAnnotationConfigDispatcherServletInitializer, respectively.
public class TenantFilter implements Filter {
private final Pattern pattern = Pattern.compile(";\\s*tenant\\s*=\\s*(\\w+)");
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String tenant = matchTenantSystemIDToken(httpRequest.getRequestURI());
Tenant.setCurrentTenant(tenant);
try {
chain.doFilter(request, response);
} finally {
Tenant.clearCurrentTenant();
}
}
private String matchTenantSystemIDToken(final String uri) {
final Matcher matcher = pattern.matcher(uri);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
}
Tenant class is a simple wrapper around a static ThreadLocal.
public class Tenant {
private static final ThreadLocal<String> TENANT = new ThreadLocal<>();
public static void setCurrentTenant(String tenant) { TENANT.set(tenant); }
public static String getCurrentTenant() { return TENANT.get(); }
public static void clearCurrentTenant() { TENANT.remove(); }
}
Just as your code I`m not setting a defaultTargetDataSource on my AbstractRoutingDataSource.
public class CustomRoutingDataSource extends AbstractRoutingDataSource {
#Override
protected Object determineCurrentLookupKey() {
if(Tenant.getCurrentTenant() == null) {
return "TENANT1";
}
return Tenant.getCurrentTenant().toUpperCase();
}
}
Now you can switch datasource with http://localhost:8080/sandbox/myEntities;tenant=tenant1. Beware that tenant has to be set on every request. Alternatively, you can store the tenant in the HttpSession for subsequent requests.
Here is a complete example: https://github.com/ksokol/spring-sandbox/tree/sdr-routing-datasource-url/spring-data