javaspringjpacriteriabuilder

java CriteriBuilder mask column value


I use Spring webcontroller with security. When the user has or has got some privileges I want to mask some values as NULL. I use JpaRepository.findAll with Specification. Basically, I want to add some logic into Criteriabuild to return some custom DEFAULT value for a column.

Let's say I have this table CODE_TABLE:

ID , CODE
=========
1 , CODE1
2 , CODE2

And I want to achieve this query with Specification:

SELECT ID, 'DEFAULT' as CODE
FROM CODE_TABLE

Solution

  • When the user has or has got some privileges

    If you are trying to hide some values for currently authenticated object in SecurityContext you should take advantage of MethodAuthorizationDeniedHandler and @AuthorizeReturnObject annotation.

    First create custom annotation that will handle the result

    @Retention(RetentionPolicy.RUNTIME)
    @PostAuthorize("true")
    @AuthorizeReturnObject
    @interface PostReadProtect {}
    

    then override methods with specification parameters in your repository

    interface FooRepository extends JpaRepository<Foo, Long>, JpaSpecificationExecutor<Foo> {
    
      @Override
      @PostReadProtect
      List<Foo> findAll(Specification<Foo> specification);
    }
    

    create handler i mentioned earlier

    @Component
    class ProtectedHandler implements MethodAuthorizationDeniedHandler {
    
      @Override
      public Object handleDeniedInvocation(
                    MethodInvocation methodInvocation,
                    AuthorizationResult authorizationResult) {
        return "[PROTECTED]";
      }
    }
    

    and protect fields in your entity

    @Getter
    @Setter
    @Entity
    class Foo {
    
      @Id
      @GeneratedValue
      private Long id;
      private String col1;
    
      @PreAuthorize("false")
      @HandleAuthorizationDenied(handlerClass = ProtectedHandler.class)
      public String getCol1() {
        return col1;
      }
    }
    

    You can you any Spring expressions ins Pre/PostAuthorize annotation as you are used to. So in our example with @PostAuthorize("true") we allow to invoke findAll(spec) for anyone, however accessing col1 via getter will trigger AccessDeniedException because of @PreAuthorize("false") which is handled by ProtectedHandler then you run your code for example. Because everything is handled by Spring Security, it gives you a lot of options how to customize logic based on current Authentication

    final var e = new Foo();
    e.setCol1("password");
    
    fooRepository.save(e);
    
    final var ctx = SecurityContextHolder.createEmptyContext();
    ctx.setAuthentication(new UsernamePasswordAuthenticationToken("fake", "login"));
    SecurityContextHolder.setContext(ctx);
    
    final var res = fooRepository.findAll((r, cq, cb) -> cb.le(r.get("id"), 500L));
    // outputs [PROTECTED] even we saved it with "password" value
    System.out.println(res.getFirst().getCol1());
    

    Alternatively you can manage entire process manually using AuthorizationAdvisorProxyFactory class.

    Read more at official documentation https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#_using_the_denied_result_from_the_method_invocation