0

Idea is simple. I have a object and I would like to hide some fields based on the some specific roles.

I have roles in the system "dog", "cat" etc.

class Food{

  String name;

  @HideInfoForTheRoles({"dog", "cat"})
  String age;
}

So I think to create something like that:

public String hideForRole(T object, String role){
// return new json
}

Or maybe I can override some denationalization method to force Jackson to hide field based on my annotation?

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
grep
  • 5,465
  • 12
  • 60
  • 112

2 Answers2

0

You could use @JsonView. That's probably the easiest solution, as @JsonView works out-of-the-box with JAX-RS.


Alternativerly, it could be achieved with a BeanPropertyFilter, similar to another solution I put together a while ago.

Start defining your annotation:

@Documented
@Retention(RUNTIME)
@Target({FIELD})
public @interface HiddenForRoles {

    String[] value();
}

Then define your BeanPropertyFilter, which can extend SimpleBeanPropertyFilter:

public class HiddenForRolesPropertyFilter extends SimpleBeanPropertyFilter {

    private String allowedRole;

    public HiddenForRolesPropertyFilter(String allowedRole) {
        this.allowedRole = allowedRole;
    }

    @Override
    public void serializeAsField(Object pojo, JsonGenerator jgen,
                                 SerializerProvider provider,
                                 PropertyWriter writer) throws Exception {

        HiddenForRoles hiddenForRoles = writer.getAnnotation(HiddenForRoles.class);
        if (hiddenForRoles != null) {
            if (Arrays.asList(hiddenForRoles.value()).contains(allowedRole)) {
                writer.serializeAsOmittedField(pojo, jgen, provider);
                return;
            }
        }

        // If no annotation is provided, the property will be serialized
        writer.serializeAsField(pojo, jgen, provider);
    }
}

Place the @HiddenForRoles annotation in your fields, according to your needs and ensure the class is annotated with @JsonFilter:

@Data
@JsonFilter("hiddenForRolesPropertyFilter")
public class Foo {

    private String bar;

    @HiddenForRoles({"cat"})
    private String biz;
}

Finally, register the filter in a ContextResolver for ObjectMapper:

String currentUserRole = // Get role from the current user

FilterProvider filterProvider = new SimpleFilterProvider()
        .addFilter("hiddenForRolesPropertyFilter",
                new HiddenForRolesPropertyFilter(currentUserRole));

ObjectMapper mapper = new ObjectMapper();
mapper.setFilterProvider(filterProvider);

If you want to make your filter "global", that is, to be applied to all beans, you can create a mix-in class and annotate it with @JsonFilter:

@JsonFilter("hiddenForRolesPropertyFilter")
public class HiddenForRolesPropertyFilterMixIn {

}

Then bind the mix-in class to Object:

mapper.addMixIn(Object.class, HiddenForRolesPropertyFilterMixIn.class);
cassiomolin
  • 124,154
  • 35
  • 280
  • 359
0

Create annotation that supports on FIELD and METHOD

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface HideFor{
    String[] roles() default{};
}

and logic that supports annotation for both field and methods

public class AccessRestrictionFilter extends SimpleBeanPropertyFilter {


    @Override
    public void serializeAsField(Object pojo, JsonGenerator jgen, SerializerProvider provider, PropertyWriter writer)
            throws Exception {

        if(writer.getAnnotation(HideFor.class)!=null && isHidable( Arrays.asList(writer.getAnnotation(HideFor.class).roles()))){
            logger.debug("Found restriction on the getter method of the field: " + pojo + " Restriction For" + Arrays.toString(writer.getAnnotation(HideFor.class).roles()) );
                 return;
        }

        Field[] fields = jgen.getCurrentValue().getClass().getDeclaredFields();

        Optional<Field> field = Arrays.stream(fields)
                .filter(f-> f.getName().equalsIgnoreCase(writer.getName())).findAny();

        if(field.isPresent() && field.get().getAnnotation(HideFor.class)!=null){
            if(isHidable( Arrays.asList(writer.getAnnotation(HideFor.class).roles()))){
                System.out.println("Found restriction on the field " + field.get().getName() + " Restriction For " + Arrays.toString(writer.getAnnotation(HideFor.class).roles()));
                return;
            }
        }
        writer.serializeAsField(pojo, jgen, provider);
    }

    private boolean isHidable(List<String> rolesToHide){ // imlement the logic // }

}

Usage:

    FilterProvider filterProvider = new SimpleFilterProvider().addFilter("AccessRestrictionFilter", new AccessRestrictionFilter()); 
    new ObjectMapper().writer(filterProvider ).writeValueAsString(myObjToFilter);

I use Jersey/Spring and my configuration looks like this:

@Provider
@Produces({MediaType.APPLICATION_JSON})
public class JacksonJsonProvider extends JacksonJaxbJsonProvider {
    public JacksonJsonProvider(AccessRestrictionFilter filter) {
        ObjectMapper objectMapper = new ObjectMapper()
                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .setFilterProvider(new SimpleFilterProvider().addFilter("AccessRestriction", filter));
        setMapper(objectMapper);
    }
}

And Filter:

   @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
    @Bean("accessRestrictionFilter")
    public AccessRestrictionFilter accessRestrictionFilter(){
          return new AccessRestrictionFilter();
    }

Note: in the filter I use the Security Context, because of this scope of the filter is Session (Not to share the state but create new object for each user)

and that's my POJO:

@JsonFilter("AccessRestrictionFilter")
public class MyClass {

    @HideFor(roles = {"ROLE_USER", "ROLE_EDITOR"})
    private int val;
grep
  • 5,465
  • 12
  • 60
  • 112