12

I'm building quite complex REST API. The whole API is protected with authentication. Some of the resources (like, let's say, Person) should be accessible for anyone in the system, however I'd like to "hide" some fields for specific user's roles.

Let's say the Person resource has following fields:

FirstName
LastName
BirthDate
Address

I'd like them all to be visible for users with HRManager role, hide Address for JuniorHRManager and leave FirstName + LastName for everyone else.

Is that RESTful way to just remove fields from the response based on rules applied for the role which logged in user has? This would be most simple to implement I guess (since I'm using an excellent ServiceStack which has global response filters), yet I'm not sure if that doesn't break the REST rules?

The only other way I've so far thought of is creating role-specific Resources (like PersonForHRManager etc.) however this would be ridiculous as the system is supposed to have variety of combinations of visible & hidden fields for roles.

migajek
  • 8,524
  • 15
  • 77
  • 116
  • possible duplicate of [Different REST resource content based on user viewing privileges](http://stackoverflow.com/questions/10996328/different-rest-resource-content-based-on-user-viewing-privileges) – Aziz Shaikh Dec 26 '13 at 11:27

2 Answers2

11

I agree with your approach, the response filter would probably be the best solution to do this; and simply mark up the response DTO with an attribute describing the required roles. I haven't seen a better way to do property level permissions.

It is perfectly acceptable to remove properties from response based on roles, if it is a public API just make sure to document the properties people can expect back in each role.

Response Filter, in your AppHost:

this.ResponseFilters.Add((req, res, dto) => {

    // Get the roles you are permitted to access. You will need to store these in the request Items collection
    var roles = (from r in req.Items where r.Key == "Roles" select r.Value).FirstOrDefault() as string[];

    // Get the type of the response dto
    var dtoType = dto.GetType();

    // Loop through the properties
    foreach(var property in dtoType.GetPublicProperties()){

        // Ignore properties that are read-only
        if(!property.CanWrite)
            continue;

        // Get all the role attributes on the property
        var attributes = property.GetCustomAttributes(typeof(RequireRoleAttribute), false) as RequireRoleAttribute[];

        // Get all the permitted roles
        var permittedRoles = new List<string>();
        foreach(var attribute in attributes)
            permittedRoles.AddRange(attribute.Roles);

        // Check if there are specific permitted roles assigned to this attribute
        if(permittedRoles.Count != 0)
        {
            bool permitted = false;

            // Check if check require role against roles we may have.
            foreach(var role in permittedRoles){
                if(roles.Contains(role))
                {
                    // We have a matching role
                    permitted = true;
                    break;
                }
            }

            // No permission to the property
            if(!permitted) {
                var type = property.GetType();

                // Set the field to a default value.
                property.SetValue(dto, null);
            }
        }
    }
});

The attribute:

public class RequireRoleAttribute : Attribute
{
    public string[] Roles { get; set; }
    public RequireRoleAttribute(params string[] roles) { Roles = roles; }
}

On the DTO:

[RequireRole("Spiderman","Superman","Batman")]
public string Address { get; set; }

Notes:

  • You will need to save you permitted roles into the request.Items.Add("Roles", string[])
  • I haven't tested the above code, so it may not be perfect, but it should be pretty close.

I hope this helps.

Scott
  • 21,211
  • 8
  • 65
  • 72
  • Thank you very much Scott, I was actually thinking of very similar solution. I was also considering "Fluent configuration" instead of Attributes, this would make it more complex but will keep it refactor-friendly (as opposed to "string-specified" roles). Anyway I was more interested in design aspect of this solution (i.e. what is RESTful way of such protection) ;) – migajek Dec 26 '13 at 11:22
  • 2
    I'd give one more +1 for the annotation regarding "design-pattern" aspect! thank you! – migajek Dec 26 '13 at 11:23
  • @migajek You're welcome. I am trying to find a source to verify this an acceptable RESTful approach. I have seen it implemented this way, but I don't remember where. – Scott Dec 26 '13 at 11:30
3

I think you got it right. RESTful services is nothing more than simple HTTP requests. Therefore, you have to protect http resources as a whole. I see two possible strategies:

  1. Implement different RESTful service for each role, which is the strategy you suggest. This is not very easy to maintain, as you probably understand.

  2. Implement one RESTful service which returns for all roles but you would have to check for authorization while forming each property value. For example, when you shape the value to be returned for the property Address on the server side, you will have to check the user's role. If the user is an HRManager you would return the right value. If the user is a JuniorHRManager you would have to return the specific property empty or with a respective message indicating that the user is not allowed to access the specific property. This strategy could be easier to maintain, depending on the server side technology used to implement the web service (I don't know which you are using). In .NET for example you could use attributes for each property indicating which authorization roles have access to a specific property. If you use .NET, this and this tutorials could provide some extra guidance.

Hope I helped!

Pantelis Natsiavas
  • 5,293
  • 5
  • 21
  • 36
  • Thank you Panetlis, I was considering both solutions. This question is more about "is that OK with REST pattern" rather than the implementation specifics, but it seems there is no "cleaner" solution – migajek Dec 26 '13 at 11:20