Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should I have multiple views/endpoints of a resource in a RESTful service?

Let's say I'm creating a RESTful service to handle orders for my warehouse over the web.

  • I want to allow customers to create accounts
  • I want a customer admin to be able to create accounts for other users in their office
  • I want to allow customer users to create orders
  • I want a site administrator to be able to create and manage all customer accounts
  • I want a site administrator to be able to create and manage all users
  • I want a site administrator to be able to create and manage all orders

Given these requirements. My initial thoughts are to design endpoints in this manner.

# to request a new customer account
/customers/request {POST}
# create and view customers - limited to admins
/customers {GET, POST}
# view customer info, update a customer
/customers/{customer_id} {GET, PATCH}
# create and view orders for a customer
/customers/{customer_id}/orders {GET, POST}
# view and update order for a customer
/customers/{customer_id}/orders/{order_id} {GET, PATCH}

I feel pretty confident that those path's make sense and follow the general restful ideas. However, I'm not sure how to handle the users endpoint. The problem is, I want customer admins to be able to create users that can use their customer account to create orders. Where do customer admins POST to to accomplish this? I had a couple of ideas. Following this answer, I thought about this.

# creation of users always done through this endpoint no matter what the
# authenticated user's role is
/users { GET, POST }
# associate user with customer
/customers/{customer_id}/user_memberships { GET, POST }

The problem with this approach is how does the admin of the customer account get the ID of the user to associate with the customer account. Any GET request on /users would be filtered by retrieving only users who are part of their customer account. However, because the user would be created before the membership, they would never be able to view the user. I also though about just having two endpoints to create users.

# create a user for a customer account
/customers/{customer_id}/users {GET, POST}
# root users endpoint only accessible to admins
/users {GET, POST}
# return same user
/users/1
/customers/{customer_id}/users/1

It essentially boils down to using the customer url prefix as a means of authorization. It seems a little strange to have two endpoints invalidating the other. What if the root endpoints were only views of the subresource endpoints?

# view all users in system - admin only
/users {GET}
# create & view admin users
/admin/users {GET, POST}
# create internal office users
/locations/{location_id}/users { GET, POST }
# create customer users
/customers/{customer_id}/users { GET, POST }

In this case, we could still cache GET responses on the sub resources as they would not change unless there was a POST or PATCH/DELETE on the specific id of a subresource.

This style also seems to make sense for orders. Admins can view all orders even though they technically belong to a customer.

# admin can view all orders
/orders?customer_id=1234
/orders

I kind of like the idea of the root resource being a view of subresources allowing for easier authorization based on the url.

So, I guess after all of that, my real question is:

Is having multiple endpoints representing the same resource a problem even if one of them is just an aggregate view of the subresources and does not permit the creation of a resource through that endpoint?

like image 885
chrislbs Avatar asked Jan 03 '14 17:01

chrislbs


1 Answers

You shouldn't mix the design of your API, REST principles, and the need for authorization. You should design your API in a way that makes it:

  • easy to use
  • easy to maintain
  • easy to understand

A RESTful approach to API design tries to address these different concerns. A RESTful approach is about identifying the objects you have, their state, and their possible transition.

And that's where it stops. Now, you wonder about authorization. You want to be able to control what a user can do on given records depending on who the user is (an administrator, a customer,...) and what the targeted resource is (a customer record...).

What you need to do is deploy an authorization framework on top of your REST API in a loosely-coupled way. In other words, you want to externalize authorization. You definitely not want to build authorization straight into your API. Imagine that suddenly you have new authorization rules / constraints: you would have to recode your API. In doing so you'd break all the clients. That would lead to poor user experience.

So, we've identified you need to externalize authorization. Great. What are the different ways to do so? This depends on the language and framework you use.

You can use:

  • Spring Security in Java
  • Yii in PHP
  • CanCan in Ruby
  • ... and many more

You could also implement your own filters, for instance a Servlet filter in Java in front of your REST endpoints.

Lastly, you can turn to a full-blown attribute-based authorization model based on XACML. There are several open-source and vendor alternatives. If you are not familiar with attribute-based access control or XACML, have a look at the following links:

  • ABAC explained by NIST
  • XACML

With XACML, you define policies centrally e.g:

  • Administrators can view all customer accounts
  • Administrators can modify a customer account he/she is assigned to
  • Customers can view and edit their own account only

The policies are then evaluated in an authorization service (in XACML that's known as a policy decision point). The authorization service exposes a binary authorization API which your API can call out to: can user Alice view record foo?.

Using externalized authorization based on policies and using XACML, you achieve a loose coupling between your business logic (your business API) and the authorization logic which you can more easily maintain and update.

like image 125
David Brossard Avatar answered Sep 22 '22 15:09

David Brossard