#SpEL

190 messages · Page 1 of 1 (latest)

acoustic mortar
#

Been trying to understand how to work with the spel expressions.

    @PostMapping("/wtf")
    @PreAuthorize("partOfBU(businessUnitDTO.id())")
    public ResponseEntity<Object> aaa(@RequestBody BusinessUnitDTO businessUnitDTO){
        return new ResponseEntity<>(HttpStatus.OK);
    }

rn I have this method. Inside the @PreAuthorize this is a spel expression.

  ExpressionParser parser = new SpelExpressionParser();
  Expression expression = parser.parseExpression(invocation.getMethod().getAnnotation(PreAuthorize.class).value());
  
  parser.parseExpression(expression.getExpressionString()).getValue(this);

I really have no idea what I'm really doing with this. All I know is I'm calling the method inside (which is what I was trying to do).
The problem is the expression is coming back as partOfBU(bussinessDTO.id()) and not partOfBU(1) for example (Yes the object is correctly de-serialized and everything. I've debugged it way to many times at this point). Anyone who has any idea?

deep quailBOT
#

This post has been reserved for your question.

Hey @acoustic mortar! Please use /close or the Close Post button above when your problem is solved. Please remember to follow the help guidelines. This post will be automatically closed after 300 minutes of inactivity.

TIP: Narrow down your issue to simple and precise questions to maximize the chance that others will reply in here.

lyric mica
#

not sure whether this is the problem but it might happen that Spring doesn't know the name of the parameter

#

because normally, Spring doesn't have access to the parameter name

acoustic mortar
#

I tried doing @P("bu") @RequestBody BusinessUnitDTO businessUnitDTO) and using the bu in the annotation above

#

same result

#

(btw intellij is giving me autocomplete on those)

lyric mica
#

what exactly is the issue?

#

Like what happens?

acoustic mortar
#

when I'm parsing that thing I'm supposed to be getting the id of the bu

#

but I just get the text - bu.id() <- this text (or whatever is above)

lyric mica
#

which text?

acoustic mortar
#

it doesn't translate to a number and it just "runs" the method with a string bu.id

#

actually no

#

it tries to call bu.id but afterwards when it doesn't have access to it anymore*

lyric mica
#

you might need #{} or similar around it

acoustic mortar
#

around the whole thing or just bu.id()?

lyric mica
#

around the whole thing inside the string

acoustic mortar
#

@PreAuthorize("partOfBU(#{businessUnitDTO.id()})")

#

org.springframework.expression.spel.SpelParseException: Expression [partOfBU(#{businessUnitDTO.id()})] @10: EL1043E: Unexpected token. Expected 'identifier' but was 'lcurly({)'

#

was thrown by the bottom line

lyric mica
#

I meant including the partOfBU think

acoustic mortar
lyric mica
#

but I was just guessing with that anyways

lyric mica
acoustic mortar
#

org.springframework.expression.spel.SpelParseException: Expression [#{partOfBU(businessUnitDTO.id())}] @1: EL1043E: Unexpected token. Expected 'identifier' but was 'lcurly({)'

#

got pretty much the same exception

lyric mica
#

that doesn't have access to the parameters

#

Why should it?

acoustic mortar
#

I was expecting that this "calculation" will be done before hand

#

I think I have another way to work around this (using the MethodInvocation). Expected this to be a simple issue which is solved easily but I guess it's more complex than that

lyric mica
#

Why should your parser thing know about the parameter?

acoustic mortar
#

you didn't get me

#

I was expecting the value to have already transformed before it gets to the parser

lyric mica
#

No, I didn't

#

why would it?

#

that isn't how annotations work

#

the parser parses it and can then maybe use the parameter - if the parser has access to the parameter

#

What's that code even supposed to do?

acoustic mortar
#

I made my own authorization manager

#

just trying to figure out how to pass stuff to it

#

otherwise it works when I provide it static values

lyric mica
#

and you are using a custom parser object for that?

#

Can you show the whole method?

#

like the method with the parser

acoustic mortar
#
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
        
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(invocation.getMethod().getAnnotation(PreAuthorize.class).value());
        //Not used. Was trying to random things
        //EvaluationContext ec = SimpleEvaluationContext.forReadOnlyDataBinding().build();
        
        parser.parseExpression(expression.getExpressionString()).getValue(this);

        Arrays.stream(invocation.getArguments()).findFirst();

        AuthorizationDecision ad;
        if(onlyBUCheck){
            ad = buCheck(authentication.get());
        } else {
            ad = authorityCheck(authentication.get());
        }

        return ad;
    }
lyric mica
#

what exactly is MethodInvocation?

acoustic mortar
#

it has all the details of the method. That would've been my workaround.

lyric mica
#

is that from AOP?

acoustic mortar
#

lemme debug it and show

#

yep

lyric mica
#

org.aopalliance.intercept.MethodInvocation?

#

if so, you can probably use getArguments()

acoustic mortar
#

yep that would be my workaround. Was thinking that I was doing the spel part wrong and could avoid setting stuff manually

lyric mica
#

well if you create your own parser, you'd need to set them manually

#

but maybe you can avoid creating your own parser instance

acoustic mortar
#

I was trying to copy from an implementation of AuthorizationManager (PreAuthorizeAuthorizationManager) and they are using it (ofc passing it through like 5 classes to parse it but still)

#

didn't think of trying it up until now but it has the same behavior

#

it gets what's in there as a string and nothing is done automatically

lyric mica
#

just add your own security expressions?

#

oh and you don't need #{}

#

but you need # before your parameter

#

I think

acoustic mortar
#

will try them out tmr. thanks for the recommendation

deep quailBOT
deep quailBOT
#

💤 Post marked as dormant

This post has been inactive for over 300 minutes, thus, it has been archived.
If your question was not answered yet, feel free to re-open this post or create a new one.
In case your post is not getting any attention, you can try to use /help ping.
Warning: abusing this will result in moderative actions taken against you.

acoustic mortar
#

read the things you sent

#

tried implementing a part

#

even found something about it in the current version of the documentation (at the bottom)

#

but then I thought. Why don't I try doing the same thing with the already existing preAuthorize implementation

lyric mica
#

this is what I was talking about

acoustic mortar
#
    @PostMapping("/wtf")
    @PreAuthorize("hasAnyRole(bu.name())")
    public ResponseEntity<Object> aaa(@P("bu")@RequestBody BusinessUnitDTO businessUnitDTO){
        return new ResponseEntity<>(HttpStatus.OK);
    }
lyric mica
#

you can create your own method and make it available in @PreAuthorize

acoustic mortar
lyric mica
acoustic mortar
#

I tried that too

#

same issue

lyric mica
#

With the standard @PreAuthorize?

acoustic mortar
#

yeah

#

trying it again rn to confirm

lyric mica
#

and you removed your custom parsing code?

acoustic mortar
#

yes the default stuff

#

(still no exceptions are thrown cuz it's expecting a string and I'm giving it that)

#

this it the method doing the evaluation

lyric mica
#

first try without a custom evaluation

#

Does that work?

acoustic mortar
acoustic mortar
acoustic mortar
# acoustic mortar

these are the expression and the evaluationContext which are passed to the above method - evaluateAsBoolean(...)

lyric mica
acoustic mortar
#

what?

#

thought of putting a break point on the inside method. It is getting called. I guess I just can't see where

lyric mica
#

yeah but what do you want to tell me with that view?

acoustic mortar
# acoustic mortar

with this image? see the 2nd row. The expression isn't translated. So even if it calls it later it should throw the error that bu doesn't exist (cuz it doesn't know about it).

#

but I guess it just keeps that bu object (or the value itself) somewhere else as no exceptions are thrown

lyric mica
#

so what happens?

acoustic mortar
#

it gives me access denied cuz I'm sending a role that doesn't exist

#

will try to modify it rn to 100% confirm that bu.name() is actually translated to a value inside

lyric mica
acoustic mortar
#

yes. but I can't 100% confirm it yet. I'll try passing in a role that exists and see what happens then

acoustic mortar
#

there is something weird happening

#

Supplier<Authentication> authentication this thing that is passed to the check method is empty

#

(in the SecurityContextHolder everything is there as expected)

#

otherwise I'm still getting access denied even though the correct role is provided and in the context

acoustic mortar
#

no 1 sec

lyric mica
#

or empty as in it doesn't give you what you expect when calling it?

acoustic mortar
lyric mica
#

don't check it in the debugger

#

you'd need to run it

acoustic mortar
#

ok will try to print it out then

lyric mica
#

you'd need to call authentication.get()

#

and that gives you the Authentication

#

Supplier is a functional interface

acoustic mortar
#

ok everything is in there

#

and now this
I'm still getting access denied even though the correct role is provided and in the context

#

can confirm the .name() method is called from somewhere

#

this is the object I'm sending (and the controller is parsing correctly)

#

this is the role I'm putting in (also tried inserting it in lower case to see if it made a difference but it didn't)
(this is during login)

#

it just has a name field and implements GrantedAuthority

#

this is the controller method with the PreAuthorize

#

.name() method returns String

lyric mica
#

ok so what happens here?

#

and what should happen?

acoustic mortar
#

still getting access denied even though I have the correct role

#

this is using everything default

lyric mica
#

like where are you adding the role to?

acoustic mortar
lyric mica
#

What's your authentication object?

acoustic mortar
#

basic username and password

lyric mica
acoustic mortar
#

the rest are business logic stuff but sure

lyric mica
#

I'm interesting in the stuff from UserDetailService

#

the methods you have overridden from there

acoustic mortar
#

that's the only method

lyric mica
#

ah wrong class

#

I meant SecurityUser

acoustic mortar
#
public class SecurityUser implements UserDetails {

    private User user;
    private List<GrantedAuthority> roles;

    public SecurityUser(User user, List<GrantedAuthority> roles) {
        this.user = user;
        this.roles = roles;
    }

    @Override
    public List<GrantedAuthority> getAuthorities() {
        return roles;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getEmail();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
lyric mica
#

try replacing hasRole with hasAnyAuthority

acoustic mortar
#

oh fuck right

#

it prefixes it with ROLE_

lyric mica
acoustic mortar
#

it works

#

ok so the default implementation calls those inside methods somewhere and it really gets a value and is correct

lyric mica
#

don't use another implementation

#

but add your own methods to it

acoustic mortar
#

it's final

#

can't extend it

lyric mica
#

you don't need to extend it

#

you just need to add a method available in the string supplied to PreAuthorize

#

right?

acoustic mortar
#

oh you mean the implemented expression "parsers"

lyric mica
#

use the existing parser

acoustic mortar
#

from what I remember they had default for an access modifier so I can't use them in my packages

#

lemme double check

acoustic mortar
#

I can't use them cuz I don't have the same package name

lyric mica
#

you can't use what?

#

You want to add your own method to PreAuthorize, right?

acoustic mortar
#

yes. Managed to get to somewhere then went on to test if it actually works with the default implementation

lyric mica
#

also you can use composition instead of inheritence if necessary

#

for getting around the "issue" with final

deep quailBOT
#

💤 Post marked as dormant

This post has been inactive for over 300 minutes, thus, it has been archived.
If your question was not answered yet, feel free to re-open this post or create a new one.
In case your post is not getting any attention, you can try to use /help ping.
Warning: abusing this will result in moderative actions taken against you.

acoustic mortar
#

Well managed to get it working. I'll post it here in case anyone is having the same problem in the future. This might be an old way to handle things (from what I understood from the spring docs https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#subclass-defaultmethodsecurityexpressionhandler)

Now I followed the guide dan1st suggested pretty closely doing modifications on my own. https://medium.com/@islamboulila/how-to-create-a-custom-security-expression-method-in-spring-security-e5b6353f062f

I did it with a custom AuthorizationManager. There are other ways to substitute that part but this is how I did it.

Firstly we start with a configuration class. (if you don't know wtf that is google spring configuration class

@Configuration
@EnableMethodSecurity(prePostEnabled = false) //1
class MethodSecurityConfig {

    @Bean
    public Advisor preAuthorize(CustomAuthorizationManager manager) { //2
        return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
    }

}
  1. When we enable method security spring has a default implementation. And we want to overwrite it. So we disable it.
  2. This is the method that "injects" the CustomAuthorizationManager in the correct place. THIS ONLY DOES THE PreAuthorize METHOD. If you want to use the others you'll have to overwrite their methods as well. (Shown in old docs here: https://docs.spring.io/spring-security/reference/5.8/servlet/authorization/method-security.html#jc-method-security-custom-authorization-manager) For some reason in the newer versions they decided to make the example smaller and thus harder to understand

Now how to make a custom AuthorizationManager. I was trying to copy from a few other implementations and I arrived at the conclusion that there is a difference between the method ones and the ones defined in the config class

method ones: @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter and others...

#

config ones: The ones defined in a configuration class like so

@Configuration
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
          .authorizeHttpRequests(requests -> {
                    requests.anyRequest().hasAuthority("test"); //.hasAuthority() here is what I'm talking about
          }).build();
    }
#

If you want to do config ones I'd say try to copy from the AuthenticatedAuthorizationManager implementation. I haven't really delved too deep into those as I needed to do a method one.

#

If you want to do a method one I was copying from PreAuthorizeAuthorizationManager. It looks pretty complex but I'll pass down everything I learned the last few days thinkering with it.

#

The way I wanted to authorize my app I had two choices. Either keep a ton of info in the SecurityContextHolder or query the db each request which needed authorization. I chose the 2nd option.

@Component("customAuthorizationManager")
public class CustomAuthorizationManager implements AuthorizationManager<MethodInvocation> {

    private final RoleService roleService; 
    //Injecting RoleService
    public CustomAuthorizationManager(RoleService roleService) {
        this.roleService = roleService;
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {

        ExpressionParser parser = new SpelExpressionParser(); //1
        Expression expression = parser.parseExpression(invocation.getMethod().getAnnotation(PreAuthorize.class).value());//2

        CustomMethodSecurityExpressionHandler c = new CustomMethodSecurityExpressionHandler(roleService); //3
        EvaluationContext ec = c.createEvaluationContext(authentication,invocation); //4
        boolean granted = Boolean.TRUE.equals(expression.getValue(ec, Boolean.class)); //5

        return new AuthorizationDecision(granted);
    }
}

Now the check method is the one that will be called. (Even if you call any other methods in the @PreAuthorize it wouldn't care)

So after I got that out of the way let me try to explain wtf is happening inside.

  1. I'm creating parser which will read the SpEL expression we have provided inside the @PreAuthorize
  2. Then with this complex thingy (and the parser we just created) we are getting the expression and saving it in a variable.
    3 and 4. Now we have to implement how we'll handle the expression (I'll show the implementations later)
  3. Afterwards we use the implementations we created at 3 and 4 to make a decision and return true or false #general message (this should explain Boolean.TRUE.equals(...))
umbral charmBOT
#

anyone know what Boolean.TRUE.equals(...) does?

acoustic mortar
#

I followed the above mentioned guide dan provided. Basically modifying what was there to my needs.

public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {

    private final RoleService roleService;
    //Injecting the RoleService again (manually this time as this isn't a component)
    //and cuz it's needed further down the line
    public CustomMethodSecurityExpressionHandler(RoleService roleService) {
        this.roleService = roleService;
    }

    @Override
    public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication, MethodInvocation mi) {
        StandardEvaluationContext context = (StandardEvaluationContext) super.createEvaluationContext(authentication, mi);

        MethodSecurityExpressionOperations delegate = (MethodSecurityExpressionOperations) context.getRootObject().getValue();

        CustomMethodSecurityExpressionRoot root = new CustomMethodSecurityExpressionRoot(delegate.getAuthentication());
        root.setRoleService(roleService);
        context.setRootObject(root);

        return context;
    }

}

Now the method we are overriding is where this differs from the article. I guess it might work with the same thing that article is using but I haven't tried it (cuz that implementation was supposed to be for @EnableGlobalMethodSecurity but in spring security 6 it's suggested to use @EnableMethodSecurity)
I did it that way cuz that's how it was suggested in the newest spring docs (https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#subclass-defaultmethodsecurityexpressionhandler) (if you read more into it I think it's advised against doing it those 2 ways but eh kekw)
I guess most of the method is telling spring to use the custom thingies we created instead of the defaults

#

You can also see the last custom thing we'll need to implement - CustomMethodSecurityExpressionRoot.

public class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {

    private RoleService roleService;
    //No idea why these are needed. Just followed the guide
    private HttpServletRequest request;
    private Object filterObject;
    private Object returnObject;
    private Object target;

    public CustomMethodSecurityExpressionRoot(Authentication authentication) {
        super(authentication);
    }
    
    //Here is where you add the methods you'll be using in @PreAuthorize together with their logic

    //Custom method you'll be using in @PreAuthorize
    public boolean partOfBU(Long buIdToCheck){
        //your logic
    }
    
    //Custom method you'll be using in @PreAuthorize
    public boolean authorityCheck(Long buIdToCheck, String authorityToCheck){
        //your logic
    }

    public void setRoleService(RoleService roleService){
        this.roleService = roleService;
    }

    @Override
    public void setFilterObject(Object filterObject) {
        this.filterObject = filterObject;
    }

    @Override
    public Object getFilterObject() {
        return this.filterObject;
    }

    @Override
    public void setReturnObject(Object returnObject) {
        this.returnObject = returnObject;
    }

    @Override
    public Object getReturnObject() {
        return this.returnObject;
    }

    @Override
    public Object getThis() {
        return target;
    }
}
#

Basically here's where you'll be adding the custom methods you'll be using

#

no idea why the other stuff is needed

#

And that's the whole shabang. Probably the way I've done is old but it at least works thumbsupcat