Search code examples
springspring-mvcspring-securityexpressionspring-el

NullPointerException in spring security PreFilter with two argument method


I'm using Spring 4.1.7.RELEASE with Spring Security 4.0.2.RELEASE.

security.xml:

...
<global-method-security pre-post-annotations="enabled"  />
<http auto-config="true" use-expressions="true">
    <form-login login-page="/user/login" />
    <logout invalidate-session="true" logout-success-url="/user/login?logout"/>
    <intercept-url pattern="/admin**" access="hasRole('ROLE_ADMIN')" />
    <intercept-url pattern="/user/login**" access="permitAll" />
    <intercept-url pattern="/login**" access="permitAll" />

    <intercept-url pattern="/user/create" access="hasAuthority('user_access_full')"/>

    <csrf />
</http>

<authentication-manager>
    <authentication-provider user-service-ref="myUserDetailsService" >
        <password-encoder hash="bcrypt" />    
    </authentication-provider>
</authentication-manager>
<beans:bean id="myUserDetailsService" class="my.service.MyUserDetailsService" autowire="byType"/>
...

I want to use @PreFilter to filter the user service input for assigning roles to the new user.

entity: JRole.java:

@Entity
public class JRole implements Serializable {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;

   public Long getId() {
       return id;
   }

   public void setId(Long id) {
       this.id = id;
   }

   @NotEmpty
   @Column(nullable = false, unique = true)
   private String name;
   private String displayName;

   public JRole(){

   }
   public JRole(String name) {
       this.name = name;
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }

   public String getDisplayName() {
       return displayName;
   }

   public void setDisplayName(String displayName) {
       this.displayName = displayName;
   }
}

UserService.java:

...
@Transactional
@PreFilter(value = "hasRole('admin') or (filterObject.name!='admin' and filterObject.name!='office_admin')")
public void create(JUser user, Set<JRole> roles) {
    JUser u2 = userDao.findByUsername(user.getUsername());
    if (u2 != null) {
        throw new MessageException("username duplicate!");
    }
    if (roles == null || roles.isEmpty()) {
        throw new MessageException("empty roles");
    }
    user.setRoles(roles);
    doCreate(user);
}

When I call the above method if my controller:

UserController:

@RequestMapping(value = "/create", method = RequestMethod.POST)
public String create2(Model model, @Valid @ModelAttribute("user") JUser user, BindingResult result) {
    addAttributes(model, CREATE);
    if (!result.hasErrors()) {
        if (user.getOfficeType() == null) {
            user.setOfficeType(office.getType());
        }
        if (user.getEnabled() == null) {
            user.setEnabled(true);
        }
        System.out.println(user.getRoles().size());
        try {
            userService.create(user, user.getRoles());// method arguments for PreFilter
            model.addAttribute("createSuccess", "کاربر به نام " + user.getFullName() + " ساخته شد.<br/>" + " نام کاربری:" + user.getUsername());
        } catch (MessageException ex) {

            String mess = NUtil.getDeepMessage(ex);
            result.rejectValue(null, "hey", mess + "salam");
        }
        System.out.println(user.getFullName());
    }

    System.out.println(result);
    return "user/manage";
}

I face this exception:

java.lang.NullPointerException
at org.springframework.security.access.expression.method.ExpressionBasedPreInvocationAdvice.findFilterTarget(ExpressionBasedPreInvocationAdvice.java:71)
at org.springframework.security.access.expression.method.ExpressionBasedPreInvocationAdvice.before(ExpressionBasedPreInvocationAdvice.java:35)
at org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter.vote(PreInvocationAuthorizationAdviceVoter.java:57)
at org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter.vote(PreInvocationAuthorizationAdviceVoter.java:25)
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:62)
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:232)
at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:64)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:281)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:653)
at ir.iais.nezarat.service.UserService$$EnhancerBySpringCGLIB$$3a7efc2e.create(<generated>)
at ir.iais.nezarat.controller.UserController.create2(UserController.java:105)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:221)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:137)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:776)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:705)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:959)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:967)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:869)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:644)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:843)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:725)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:291)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.netbeans.modules.web.monitor.server.MonitorFilter.doFilter(MonitorFilter.java:393)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(Applicatio ...
...
...

NullPointerException occurs at this line:

//user is not null, user.getRoles() is not null
userService.create(user, user.getRoles());

Please help!


Solution

  • Although Spring Security reference says:

    You can also filter before the method call, using @PreFilter, though this is a less common requirement. The syntax is just the same, but if there is more than one argument which is a collection type then you have to select one by name using the filterTarget property of this annotation.

    source code checks only number of arguments not the types of the arguments:

    private Object findFilterTarget(String filterTargetName, EvaluationContext ctx, MethodInvocation mi) {
        Object filterTarget = null;
    
        if (filterTargetName.length() > 0) {
            filterTarget = ctx.lookupVariable(filterTargetName);
            if (filterTarget == null) {
                throw new IllegalArgumentException(
                    "Filter target was null, or no argument with name "
                        + filterTargetName + " found in method");
            }
        }
        else if (mi.getArguments().length == 1) {
            Object arg = mi.getArguments()[0];
            if (arg.getClass().isArray() || arg instanceof Collection<?>) {
                filterTarget = arg;
            }
            if (filterTarget == null) {
                throw new IllegalArgumentException(
                    "A PreFilter expression was set but the method argument type"
                        + arg.getClass() + " is not filterable");
            }
        }
    
        if (filterTarget.getClass().isArray()) {
            throw new IllegalArgumentException(
                "Pre-filtering on array types is not supported. "
                    + "Using a Collection will solve this problem");
        }
    
        return filterTarget;
    }
    

    and Spring Security API says:

    For methods which have a single argument which is a collection type, this argument will be used as the filter target.

    [...]

    the name of the parameter which should be filtered (must be a non-null collection instance) If the method contains a single collection argument, then this attribute can be omitted.

    You have not defined a filterTarget but two arguments, so you get a NullPointerException.

    You have to add filterTarget parameter:

    @Transactional
    @PreFilter(filterTarget="roles", value = "hasRole('admin') or (filterObject.name!='admin' and filterObject.name!='office_admin')")
    public void create(JUser user, Set<JRole> roles) {
        JUser u2 = userDao.findByUsername(user.getUsername());
        if (u2 != null) {
            throw new MessageException("username duplicate!");
        }
        if (roles == null || roles.isEmpty()) {
            throw new MessageException("empty roles");
        }
        user.setRoles(roles);
        doCreate(user);
    }