33

My question is a duplicate of Custom annotation with spring security but it went unanswered and I believe there should be a simple solution to the problem.

Basically instead of doing:

@PreAuthorize("hasPermission(T(fully.qualified.Someclass).WHATEVER, T(fully.qualified.Permission).READ")

I would like to do:

@PreAuthorize(Someclass.WHATEVER, Permission.READ)

or possibly some custom annotation that will wire up easily with spring security

This seems much cleaner to me and I would like to be able to do it if I can.

Community
  • 1
  • 1
user1751547
  • 2,211
  • 4
  • 21
  • 25

6 Answers6

20

Indeed you can implement a custom strongly typed security annotation, though this is rather bothersome. Declare your annotation

enum Permission {
    USER_LIST,
    USER_EDIT,
    USER_ADD,
    USER_ROLE_EDIT
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Permissions {
    Permission[] value();
}

Declare the custom implementation of org.springframework.security.access.ConfigAttribute to be used by security pipeline

class SecurityAttribute implements ConfigAttribute {
    private final List<Permission> permissions;

    public SecurityAttribute(List<Permission> permissions) {
        this.permissions = permissions;
    }

    @Override
    public String getAttribute() {
        return permissions.stream().map(p -> p.name()).collect(Collectors.joining(","));
    }
}

Declare the custom implementation of org.springframework.security.access.method.MethodSecurityMetadataSource to create the instances of SecurityAttribute from annotations

class SecurityMetadataSource extends AbstractMethodSecurityMetadataSource {
    @Override
    public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) {

      //consult https://github.com/spring-projects/spring-security/blob/master/core/src/main/java/org/springframework/security/access/prepost/PrePostAnnotationSecurityMetadataSource.java
      //to implement findAnnotation  
      Permissions annotation = findAnnotation(method, targetClass, Permissions.class);
        if (annotation != null) {
            return Collections.singletonList(new SecurityAttribute(asList(annotation.value())));
        }
        return Collections.emptyList();
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    } 

}

At last declare the custom implementation org.springframework.security.access.AccessDecisionVoter

public class PermissionVoter implements AccessDecisionVoter<MethodInvocation> {
    @Override
    public boolean supports(ConfigAttribute attribute) {
        return attribute instanceof SecurityAttribute;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return MethodInvocation.class.isAssignableFrom(clazz);
    }

    @Override
    public int vote(Authentication authentication, MethodInvocation object, Collection<ConfigAttribute> attributes) {
        Optional<SecurityAttribute> securityAttribute = attributes.stream()
                .filter(attr -> attr instanceof SecurityAttribute).map(SecurityAttribute.class::cast).findFirst();
        if(!securityAttribute.isPresent()){
            return AccessDecisionVoter.ACCESS_ABSTAIN;
        }
        //authorize your principal from authentication object
        //against permissions and return ACCESS_GRANTED or ACCESS_DENIED

    }

}

and now bring them all together in your MethodSecurityConfig

@Configuration
@EnableGlobalMethodSecurity
class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

    @Override
    protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() {
        return new ScpSecurityMetadataSource();
    }

    @Override
    protected AccessDecisionManager accessDecisionManager() {
        return new AffirmativeBased(Collections.singletonList(new PermissionVoter()));
    }
}
Dima Korn
  • 546
  • 3
  • 5
  • Although it is indeed not a "one liner" magical answer, it still propose an excellent solution to add custom security annotation ! I've juste followed the implementation logic and it was not that hard to understand the overall logic. – Tazaf Apr 23 '21 at 10:50
16

You can create static annotations like this:

@ReadPermission

By moving @PreAuthorize annotation to @ReadPermissiondefinition:

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole(T(fully.qualified.Permission).READ.roleName())")
public @interface ReadPermission {
    
}

Benefit of this is, that you can then change Spring SPEL expression in one place, instead of modifying it on every method.

One more plus is, that you can use this annotation on Class level - every method then would be secured with this annotation. It's useful for AdminControllers etc..

DependencyHell
  • 1,027
  • 15
  • 22
Gondy
  • 4,925
  • 4
  • 40
  • 46
13

Facing the same issue, I ended up with a hybrid solution. I am using Spring-El and a custom bean to provide my own hasPermission() method which accepts an Enum. Given that Spring does an automatic string->enum conversion, at runtime, I will get a runtime exception that a particular enum does not exist if there is a typo in the string. Not the ideal solution (would have rather had something that failed at compile-time), but an acceptable compromise. It gives me some semi-type safety.

@Component("securityService")
public class SecurityService {
    public boolean hasPermission( Permission...permissions){
        // loop over each submitted role and validate the user has at least one
        Collection<? extends GrantedAuthority> userAuthorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
        for( Permission permission : permissions){
            if( userAuthorities.contains( new SimpleGrantedAuthority(permission.name())))
                return true;
        }

        // no matching role found
        return false;
    }
}

Used as follows:

@PreAuthorize("@securityService.hasPermission({'USER_ADD'})")
public User addUser(User user){
    // create the user
    return userRepository.save( user );
}

Where Permission is just a normal enum definition:

public enum Permission {
    USER_LIST,
    USER_EDIT,
    USER_ADD,
    USER_ROLE_EDIT
}

Hope this can help someone else out in the future.

royB
  • 12,779
  • 15
  • 58
  • 80
Eric B.
  • 23,425
  • 50
  • 169
  • 316
3

I did that way :

1 - Define your enum referencing a public final static String "VALUE" like this

public enum MyEnum {
    ENUM_A(Names.ENUM_A);

    private String value;

    private MyEnum (String value) {
        this.value = value;
    }

    public static class Names {

        public  final static String ENUM_A = "ENUM_A";
    }
}

2 - Concat MyEnum values in @PreAuthorize

@PreAuthorize("hasPermission('myDomain', '"+ MyEnum.Names.ENUM_A+"')")
e.g78
  • 667
  • 4
  • 8
  • 1
    When I try this way I get a compilation error of "attribute value must be constant". Did you get around that? – Scott Carlson Apr 23 '19 at 15:50
  • you get a compilation error if you concat with MyEnum.Names.ENUM_A ? Have you defined it as public final static String as in the sample ? – e.g78 Apr 23 '19 at 16:59
  • 1
    doesn't this miss the point, that i want to define my role-names only once? I have to update the static names manually – wutzebaer Dec 19 '19 at 12:44
  • That way you can define only once, in the enum – e.g78 Dec 19 '19 at 15:39
2

I created my own annotation, which gets enum in the parameters. In implemention an annotation, I created a method that gets all the roles specified in the annotation and verifies that the current user owns at least one of them. If none of the roles match, the program will throw an exception.

Enum:

public enum MyRoles {
    ADMIN("ROLE_ADMIN"),
    USER("ROLE_USER"),
    GUEST("ROLE_GUEST");

    private String name;

    private MyRoles(String name) {
        this.name = name;
    }
}

Annotation Interface:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface AllowedRoles {
    MyRoles[] value();
}

Implementation of annotation:

@Aspect
@Component
public class AllowedRolesAspect {
    @Around("@annotation(com.myproject.annotations.AllowedRoles)")
    public Object doSomething(ProceedingJoinPoint jp) throws Throwable {

        Set<MyRoles> roles = Arrays.stream(((MethodSignature) jp.getSignature()).getMethod()
                .getAnnotation(AllowedRoles.class).value()).collect(Collectors.toSet());

        HttpServletRequest httpServletRequest = getRequest();

        for(MyRoles role : roles){
            if(httpServletRequest.isUserInRole(role)){
                return jp.proceed();
            }
        }

        throw new AccessDeniedException("");
    }

    private HttpServletRequest getRequest() {

        ServletRequestAttributes servletRequestAttributes =
                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        return servletRequestAttributes.getRequest();
    }
}

Usage:

@AllowedRoles({MyRoles.ADMIN, MyRoles.USER})
@GetMapping("/myrequest/{id}")
public MyResponse getResponse(
        @PathVariable Long id
) {
    /*Do something...*/
}
cheladon
  • 121
  • 1
  • 5
0

I found this solution. It's kinda cumbersome but it's working.

public enum Type {
        MASTER_ADMIN, ADMIN, ACCESS_ADMIN, PUBLISHER, EDITOR, AGENCY, VIEWER;

        @Component("AccountRole")
        @Getter
        static class SpringComponent {
            private final Type MASTER_ADMIN = Type.MASTER_ADMIN;
            private final Type EDITOR = Type.EDITOR;
            ...
        }
    }

...

@Service("AccountRoleAccess")
public class AccountRoleAccess {
    public boolean hasAnyRole(Authentication authentication, AccountId id, AccountRole.Type... roles) {
        ...
    }
}

...

@PreAuthorize("@AccountRoleAccess.hasAnyRole(authentication, #accountId, @AccountRole.MASTER_ADMIN, @AccountRole.EDITOR)")
Semyon Kirekov
  • 1,237
  • 8
  • 20