3

I am trying to secure my service methods using @Secured as below:

public interface IUserService {

@Secured({"ROLE_ROLE1", "ROLE_ROLE2"})
    ResponseEntity saveUser(CreateUserDtoRequest userDto);

}

I wanna know is there a way to define {"ROLE_ROLE1", "ROLE_ROLE2"} in a variable and read its value from a properties file? That would be great if you can suggest me a trick, to:

  1. remove repetition of {"ROLE_ROLE1", "ROLE_ROLE2"} in other methods
  2. In case of change in required roles to access a method in future, there would be no need to change the code, recompile and deploy it again.
Morteza
  • 642
  • 7
  • 17
  • You can work with `@PreAuthorize` in your application, but you could define user permissions and add them to user as a role. User1 -> role_adming -> [save_user,edit_user]. This would look like that in your method: `@PreAuthorize(value = "hasAnyAuthority(" + "T(ua.pmdev.auctionizer.rest.user.domain.UserPermission).ADMIN.name()," + "T(ua.pmdev.auctionizer.rest.user.domain.UserPermission).ALL.name())")` – I_AM__PAUL Sep 30 '20 at 08:52
  • I do not have a problem to solve the issue with `@PreAuthorize` as there are already some solutions online. Since `@Secured` annotation is more appropriate to use in my case (simplicity and higher readability due to not having `hasRole` stuff in it in addition to not having to use `conditionalEL`), I wonder is it possible to check dynamically the `roles` in `@Secured` – Morteza Sep 30 '20 at 09:21

1 Answers1

1

There are several ways to do what you need:


Develop your custom MethodSecurityExpressionOperations

In this tutorial you will see how to deal with a new custom security method (section 5) or override the current hasAuthority one (section 6)


Develop your custom method to use in SpEL

Probably an esier option, the steps could be the following ones:

1. Include the allowed roles in your application.yml (or properties)

security:
  rolesAllowed: ADMIN,USER

2. Define the class to check those roles and authorized user ones. For example:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;

import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toSet;

@Component
public class FromPropertyRoleSecurityCheck {

  private final static String ROLE_SEPARATOR = ",";

  @Value("${security.rolesAllowed}")
  private String rawRolesAllowed;


  public boolean verifyRoles() {
    return getPrincipalAuthorities()
            .map(auth -> {
                Set<String> rolesAllowed = Stream.of(rawRolesAllowed.split(ROLE_SEPARATOR))
                        .map(String::trim)
                        .collect(toSet());
                return verifyAllowedRoles(rolesAllowed, auth);
            })
            .orElse(false);
  }


  private Optional<Collection<? extends GrantedAuthority>> getPrincipalAuthorities() {
    return ofNullable(SecurityContextHolder.getContext())
            .map(SecurityContext::getAuthentication)
            .map(Authentication::getAuthorities);
  }


  private boolean verifyAllowedRoles(final Collection<String> rolesAllowed,
                                     final Collection<? extends GrantedAuthority> principalAuthorities) {
    if (CollectionUtils.isEmpty(rolesAllowed)) {
        return true;
    }
    if (CollectionUtils.isEmpty(principalAuthorities)) {
        return false;
    }
    Set<String> rolesDiff = principalAuthorities.stream().map(GrantedAuthority::getAuthority).collect(toSet());
    rolesDiff.removeAll(rolesAllowed);
    return rolesDiff.size() != principalAuthorities.size();
  }

}

3. Add the security check:

@PreAuthorize("@fromPropertyRoleSecurityCheck.verifyRoles()")
public ResponseEntity<MyDto> findById(@PathVariable @Positive Integer id) {
  ...
}

If you don't want to recompile/deploy the project every time those roles change, you can save them in an external storage like database for example (shouldn't be a problem to update any of provided examples to deal with such situations). In the second one I used a property to keep it simple, but is quite easy to include a Repository in FromPropertyRoleSecurityCheck to get them from database.

PD. Examples of provided link and custom one were developed in Controller layer, but they should work in the Service one too.

doctore
  • 3,855
  • 2
  • 29
  • 45
  • 1
    Thanks for your response @doctore. your solution seems neat and nice and I am vote it up. Also, I will try the solution and let you know the result. – Morteza Oct 04 '20 at 09:11
  • 1
    The solution worked. thanks. just one little thing: there is no need for `ROLE_SEPARATOR` since you can define `rawRolesAllowed` as `List` instead of `String` – Morteza Oct 04 '20 at 13:30
  • Glad to help. Regarding `List`, yes, there is easier ways to work with it: https://stackoverflow.com/questions/12576156/reading-a-list-from-properties-file-and-load-with-spring-annotation-value – doctore Oct 04 '20 at 13:56