0

We have a business requirement that elements of child collections of entities (we use JPA) in our spring-boot application shouldn't be visible in rest api if the user doesn't have permissions to view child entity.

Right now we use AOP to wrap all get methods in our services so that they do something like this if (!allowed("ChildView")) {entity.setChildren(new ArrayList<>())} which doesn't seems like a good solution to me for a few reasons. First of all relationship between permission name and collections setter is hardcoded outside of entity. Also modifying actual object because we don't want to show something about it in REST api seems kind of strange. You don't remove something if you don't want to show it. You can just hide it. So I thought why not hide it when serializing?

So I can see how to ignore properties completely at runtime via Mixin and @JsonIgnore but I can't find how to return empty list instead.

Ideally I thing of an API like that.

class Entity {
    @OneToMany
    @AuthSerialize("ChildView", default=Collections.emptyList())
    Collection<Child> children;
}

Current solution looks something like this.

Map<Class<? extends BaseEntity>, Map<String, Consumer<BaseEntity>> protectors;

process(BaseEntity e) {
    protectors.getOrDefault(e.getClass(), Collectoions.emptyMap())).forEach((permission, clearer) ->
        if !allowed(permission) clearer.accept(e)
    )
user1685095
  • 5,787
  • 9
  • 51
  • 100

2 Answers2

1

I think the "not wasting cycles" is over-engineering. It might be a valid assertion if you're serializing a million entities per second. Otherwise the JVM will optimize the "hot spot" for you. And anyway, that won't be the bottleneck in your application architecture.

If you know your entities have a "children" array field in common, you might want to apply the same JsonSerializer to all of them, by simply maintining a Map of the compatible classes.

You have to understand that Jackson has its own limitations. If you need something more than that, you might want a totally custom solution. This is the best you can obtain with Jackson.


Hope the answer is satisfactory.
You can use a custom JsonSerializer<T>.

class EntitySerializer extends StdSerializer<Entity> {
    private static final long serialVersionUID = 1L;
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    EntitySerializer() {
        super(Entity.class);
    }

    @Override
    public void serialize(
            final Entity value,
            final JsonGenerator generator,
            final SerializerProvider provider) throws IOException {
        final TreeNode jsonNode = OBJECT_MAPPER.valueToTree(value);

        if (!AuthUtils.allowed("ChildView")) {
            final TreeNode children = jsonNode.get("children");

            if (children.isArray()) {
                ((ContainerNode<ArrayNode>) children).removeAll();
            }
        }

        generator.writeTree(jsonNode);
    }
}

However, as you can see we are using an ObjectMapper instance inside our JsonSerializer (or would you prefer manually "writing" each field with JsonGenerator? I don't think so :P). Since ObjectMapper looks for annotations, to avoid infinite recursion of the serialization process, you have to ditch the class annotation

@JsonSerialize(using = EntitySerializer.class) 

And register the custom JsonSerializer manually to the Jackson ObjectMapper.

final SimpleModule module = new SimpleModule();
module.setSerializerModifier(new BeanSerializerModifier() {
    @Override
    public JsonSerializer<?> modifySerializer(
            final SerializationConfig config,
            final BeanDescription beanDesc,
            final JsonSerializer<?> serializer) {
        final Class<?> beanClass = beanDesc.getBeanClass();
        return beanClass == Entity.class ? new EntitySerializer() : serializer;
    }
});

final ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);

Finally, you just have to use the ObjectMapper, or let your framework use it.
As you're using Spring, you can register a @Bean of type ObjectMapper, marked as @Primary, or you can register a @Bean of type Jackson2ObjectMapperBuilder.


Previous answer.

As the allowed method is static, that means it can be accessed from "everywhere". After fiddling a little bit with Jackson, I'll give you the first of the two options, as I'm still working on the second one.

Annotate your class with

@JsonSerialize(converter = EntityConverter.class)
public class Entity { ... }

Here you're specifying a custom Converter.

The Converter implementation is pretty neat.
Inside the static block I'm simply getting the Auth annotation value, but that is optional, you can do what you feel like is best for your usecase.

class EntityConverter extends StdConverter<Entity, Entity> {
    private static final String AUTH_VALUE;

    static {
        final String value;

        try {
            final Field children = Entity.class.getDeclaredField("children");
            final AuthSerialize auth = children.getAnnotation(AuthSerialize.class);
            value = auth != null ? auth.value() : null;
        } catch (final NoSuchFieldException e) {
            // Provide appropriate Exception, or handle it
            throw new RuntimeException(e);
        }

        AUTH_VALUE = value;
    }

    @Override
    public Entity convert(final Entity value) {
        if (AUTH_VALUE != null) {
            if (!AuthUtils.allowed(AUTH_VALUE)) {
                value.children.clear();
            }
        }

        return value;
    }
}

Let me know if this is sufficient, or you'd prefer a more complex solution.

LppEdd
  • 20,274
  • 11
  • 84
  • 139
  • Thanks for your help. Both options are interesting. However I'm not sure I've been clear enough. We have lot's of `Entity` classes and I don't want to write separate converter or custom serializer for each of them. Nor do I like wasting cycles to first serializing children and then discarding them. – user1685095 Feb 17 '19 at 13:58
  • Entity classes can have arbitrary collections of other entities named differently. Sorry if that wasn't clear. As they can be also annotated to be named differently in json I can't use field name when trying to setting collection to empty list in json. So I really need to customize serialization, not changing already serialized values. As to convertor that clears collection - this collection could be read-only, so it's not a general solution. – user1685095 Feb 17 '19 at 14:09
  • @user1685095 Inside the JsonSerializer, you can introspect the entity class via Reflection to find your custom AuthSerialize annotation. You can then extract the fields name and use them just like I did above. Obviously the Serializer will have to extend StdSerializer – LppEdd Feb 17 '19 at 14:15
  • What if the field is annotated with `@JsonProperty("foos") private List fooList`. I would need to reimplement jackson annotation processing to determine correct name use when serializing. I don't like this option. – user1685095 Feb 17 '19 at 14:23
  • @user1685095 that would require reading also that specific annotation value via Reflection. You can see that we are going on the totally-custom side of serialization. I think you need to set some constraints for your entity classes. You cannot allow everything, or the serialization process will become a nightmare. If you still want to proced on that direction, at this point I'd say, read each field via Reflection, and use the JsonGenerator which is given as input to the serialize method. A good pattern to use for that is the "Strategy" pattern, to choose the most appropriate implementation. – LppEdd Feb 17 '19 at 14:29
  • I want to customize serialization of one field not reimplement what jackson already does. So no, I don't want `totally-custom` serialization. I want to customize what jackson is doing already. If it doesn't allow this, well that sucks, but we've already have a working solution using generics that is at least type safe and isn't using reflection. If reflection would give ability to make this with less code that would be ok with me. But what you're showing uses more code that current solution already have. – user1685095 Feb 17 '19 at 14:34
  • @user1685095 what I proposed is for sure better than explicitly setting an empty Collection to a field. If you think your actual solution is good than you can post it and we can elaborate on that. If you cannot, than you're free to go deep into Jackson source code and analyze how the serialization process works line by line. But that's a task you can't delegate to others. We are giving you *possible* solutions, using our free time. Also, you're calling out for CPU cycles, but you're using AOP. That is a bit confusing. – LppEdd Feb 17 '19 at 14:37
  • I appreciate that you're trying to help. I know that I can delve into Jackson source code or spring security code, thanks. I just hoped that someone already solved this problem properly. It seems to be solved in Jersey judging by the answer to other questions. I've basic idea of our current solution. – user1685095 Feb 17 '19 at 14:47
0

You could use the Mixin to override the getter method:

class noChildViewEntity {

    public Collection<Child> getChildren() {
        return new ArrayList<>();
    }

}
Sharon Ben Asher
  • 13,849
  • 5
  • 33
  • 47