REST API Design – Handling Resource Properties Based on User Authorization

api-designgraphqlrest

I'm struggling with defining proper endpoints for a RESTful API, given the following requirements:

We have an existing /customers endpoint, which when called with the GET verb, returns a list of customers, which look like so:

{
  "id": "some-id",
  "name": "John doe",
  "email": "[email protected]",
  "address": "Somestreet 1, 1234 Somecity, Somecountry",
  "phone": "+1 234 567 765"
}

I now have a requirement to introduce a new user type to our system, these users are allowed to query the /customers endpoint, but are not allowed to see the same data about the customers:

  • emails should be masked (e.g. j***.d**@example.com)
  • address should only contain city-level information (e.g. Somecity, Somecountry)
  • Phone numbers should not be visible at all

What is, in RESTful programming, the best way to deal with these requirements? I have thought about the following approaches:

Design a completely different resource

I could design another REST resource, which would live under another endpoint like /customers-basic, and provide exactly the model that is intended for the new user type. This doesn't feel maintainable though, in an environment that is considerably complexer than the very simple example I provided, what if you have 3,4 different user types that all need to see different information about customers, I would quickly be challenged with coming up with names for all the different resource types and their routes.

Return different response types

While it's perfectly possible (using ASP.NET 6 at least) to return different models for the same resource, I think this would not be considered correct use of the REST principles. It would also make it hard to make a public specification (OpenAPI 3 e.g.) which describes which model is returned in which scenario

Making the sensitive properties sub-resources

Another solution would be to remove all sensitive properties from the customer resource, and provide them as a sub-resource of the customer (e.g. /customers/{customer-id}/email). This would solve making certain properties completely unavailable to some users (returning a 403 if they requested this protected sub-resource), but it still wouldn't solve the masking of the email property. Also this solutions would require additional API calls from the client application to completely visualize the resource. If you would need these properties in a paginated list view that would exponentially increase the number of API calls you would have to do to a point where it could potentially start getting detrimental to the applications performance.

Set unavailable properties to null

This would not solve the masking issue, but it would be a solution to properties you would want to completely deny showing to the user with limited permissions. Although in the case of optional properties (phone number e.g.) you would not be able to differentiate between not having a phone number, and the number being hidden from you.

GraphQL

Slightly off-topic, but if a different protocol does provide a solution, it should be mentioned here. I don't know a lot about GraphQL, but I do know it provides means of specifying which properties are available to a client, and then the client specifying which properties of a resource it wants to see. What I couldn't find is if it's common practice to limit a subset of properties to a higher-permission enabled user.

Any other approaches I'm missing? How do other people deal with these kind of requirements?

FYI: I'm working in a ASP.NET 6 environment, but it's really more of a design question than a technology question.

Best Answer

REST does not define behavior for such fine-grained details. Keep the same endpoint, but return different data depending on the client. If the client has authorization to view the customer, return a 200 OK response. This satisfies REST and the HTTP protocol. The specific data being returned is up to application logic.

Consider for a moment that you create a second endpoint called /customers-basic and the existing /customers endpoint returns unmasked information. What is stopping a malicious actor from calling /customers when they should be calling /customers-basic? Forget that the client should call one over the other. Forget that you might give the client the endpoint they should call. Assume that some client will attempt to access information they are not authorized to see. Authorize every request. The data returned in the request depends on the use case and permissions. REST doesn't even figure into that decision or design. Consider another example where you and I are coworkers.

Let's say I work in level 1 tech support and you work in level 2 support. I take calls first and then escalate to you if I cannot handle the problem. You reply to me saying:

Hi Greg,

I was able to resolve this customer's issue:

https://example.com/customers/123

Alex

You can see that URL because you have elevated access to the system. With two different endpoints, I should visit /customers-basic/123 due to me having lower access. The data returned by both endpoints represents the same "customer" but you force clients to understand their access to the system in order to access that resource. You also force clients to understand each others access.

If I visit the URL in your email, I would get 401 Unauthorized response, which is a frustrating thing for clients to remember. It would drive clients crazy because it is the same customer.

Remember that a resource in RESTful programming is an abstract concept. Two different clients do not necessarily need the same data bit-for-bit. Conceptually a URL should return the same object, but the exact representation of that object will depend on application logic out of the scope of REST.

Related Topic