cdnscubaguy
cdnscubaguy

Reputation: 99

Property/Field level security with REST API

I'm building a proof of concept for a REST API that supports a multi-tenant authorizations model. This model not only from controls which objects a user can access but also the fields in the object. The goal of this model is to ensure that a tenant admin can only modify their tenant as well as only see object properties that are permitted.

I have an existing code base that I am working on and is available publicly at https://github.com/cypherkey/multi-tenant-rest-api. It is based on the sample Spring OAUTH2 resource server project. I wrote my own implementation of field level security that uses analyzes the annotations on the fields in the DTO and model and if the user has sufficient rights, it uses reflections to copy data from one class to another. While this seems to work, I want to ensure I'm going down the right path. Is there a more standard Spring approach or maybe another framework that might accomplish this?

I've been researching JSONViews. They look like they would work for serialization. I would create a different view for SUPERADMIN, TENANTADMIN, and USER level roles. The controller would be responsible for determining whether the client can access the object and the JSONView would be responsible for filtering the fields/properties. The problem is I have found a few examples of supporting this for serialization but not for de-serialization on a POST/PUT at the controller level. For example:

Upvotes: 2

Views: 2237

Answers (1)

Juan
Juan

Reputation: 2099

I found this question because I stumbled with the same problem. I know this is an old question, but I'm posting my solution so maybe anyone in the same situation could find it helpful. I finally resolved it using a mixed solution based on reflection (for update requests) and Genson's API methods (for read requests).

Firstable, I defined a class which would contain a List of forbidden fields for each entity and each kind of role. When the application starts, it reads a config file which contains this information and stores it in an object of this class. This object is included to the Context, so this information is accessible from the requests of the service.

When the GET requests is called, the entity is obtained, but after that, a Genson object is created like this:

protected Genson buildRestrictedGenson(String rolename, ElementExcludedFields ef) {
    List<String> excludedFields = null;
    if(rolename.equals(Utils.ROL_ADMIN))
        excludedFields = ef.getAdminUser();
    else if(rolename.equals(Utils.ROL_BASIC))
        excludedFields = ef.getBasicUser();
    else
        return new Genson();
    GensonBuilder gb = new GensonBuilder();
    for(String field : excludedFields)
        gb.exclude(field);

    return gb.create();         
}   

Using exclude method means that when you serialize an object, the String obtained won't contain the forbidden fields. So for every GET of every entity, I just need to call this method, serialize the object obtained with this Genson object and include it in the response.

For updating is a bit more complex, because I use Hibernate for data persistance, and it needs the complete object to perform updates. So, the first step would be get the original object from database. When I have it, I can use reflection to ignore the forbidden fields, assigning the original values to this fields of the "updating" object. Something like this:

protected void excludeUpdateFields(T entity, int id, String rolename, ElementExcludedFields ef) {
    T t = getService().get(id);
    Iterable<Field> fields = ReflectionUtils.getAllFields(entity.getClass());
    List<String> fieldList;
    if(rolename.equals(Utils.ROL_ADMIN))
        fieldList = ef.getAdminUser();
    else if(rolename.equals(Utils.ROL_BASIC))
        fieldList = ef.getBasicUser();
    else
        return;

    for(Field field : fields) {
        if(fieldList.contains(field.getName())) {
            try {
                field.setAccessible(true);
                Object value = field.get(t);
                field.set(entity, value);
            } catch (IllegalArgumentException ex) {
                Logger.getLogger(Controller.class.getName()).log(Level.SEVERE, null, ex);
            } catch (IllegalAccessException ex) {
                Logger.getLogger(Controller.class.getName()).log(Level.SEVERE, null, ex);
            }

        }
    }
}

After calling this function, the updating of the forbidden fields won't have any effect, because the "updating" object will have the original values in those fields.

Upvotes: 2

Related Questions