3

I have to design REST services for security sensitive resources that require dual control. What is an elegant way to design the REST services that implement dual control?

Dual Control

With dual control I mean that a change only becomes effective if multiple people (e.g. 2) were involved in the change.

For example, I have resource called userProfile. It defines all things a user is allowed to do. If someone wants to change such a profile, it can propose a change to it. This change then has to be verified. The result of this verification is either "Approve" or "Reject". Once a change has been approved it becomes effective.

Design

I currently have a userProfile resource and a userProfileChangeProposal resource.

creating a proposal to create a userprofile happens through

POST /userprofiles

This returns the ID of the userprofile. It can now be verified using that {id}

PUT /userprofiles/{id}/changeproposal

Deleting or updating the userprofile requires a proposal again, so:

DELETE /userprofiles/{id}
PUT /userprofiles/{id}

These changes can be verified again via: (there can only be 1 proposal at the same time for a userprofile)

PUT /userprofiles/{id}/changeproposal

Issues

The thing I'm struggling with is that the rest operations seem to operate on the userprofile resource, but in fact they dont. A delete doesn't directly delete the resource. It creates a proposal to delete it. Also, this approach doesn't allow direct deletion of a userprofile.

On the other hand, if all changes occur through change proposals, then all create/delete/update actions are just

CREATE /userprofilechangeproposal

I havent seen anything on the internet regarding dual control design. The closest was that someone first creates an ORDER resource and only after the order has been approved the actual CAR is created.

Anyone has any best practices?

jannis
  • 4,843
  • 1
  • 23
  • 53

1 Answers1

7

I think this post on Rest Cookbook answers your question. The same article, but a little more verbose can be found here and here.

Summarizing the info in the URL your workflow should look like this:

  1. User sends a POST to /userprofiles with the profile info in the payload { "username": "user1298232" } (as an example)

  2. The service replies with 202 Accepted and Location: /proposals/12345 header.

  3. User can check the proposal status by sending a GET /proposals/12345 request. This resource might look like this { "status": "waiting for approval" }

  4. When the actual resource has been accepted and created (e.g. with an id of 1) GET /proposals/12345 no longer returns the status, but instead will redirect (with a 303 response status code and Location header) the request to the newly created resource at /userprofiles/1.

4a. Later on the client can either DELETE the proposal resource or the server can expire it and return a 410 Gone (after some time as an element of garbage collection).

4b. If the proposal gets rejected then its status should change accordingly (e.g. { "status": "Rejected" }).

NOTE (from the article):

Don't use a 301 or 302 when a resource has been created. These codes tell the client that the SAME resource can be found at another location. A 303 tells a client that ANOTHER resource can be found at ANOTHER location.

The same workflow can be used to modify/delete a resource.

EDIT: APPROVING PROPOSALS

I forgot to write how to approve requests. The most natural approach to this would be for the authorized user to send a partial update PATCH /proposals/1 including the status change - { "status": "approved" }.

There is a discussion on how the payload of a PATCH request should look like. The above method of describing a partial update (a partial resource including only the attributes to be updated) is implemented e.g. in the GitHub API which by many is considered the role model of a RESTful API. Presented here is another approach, which would require the payload to describe the change itself instead of the partial resource with the attributes to update: { "op": "replace", "path": "/status", "value": "approved" }.

PATCH is not yet an official HTTP method, so theoretically it may cause some problems in certain environments althought personally I haven't yet encountered such a scenario (comment under this SO post suggests that there might be a problem with PATCH in MS Edge, but I haven't checked that). Anyway you could design /proposals/[id] to accept PUT requests just to be on the safe side. Just remember that PUT should replace the resource and should contain the whole resource in the payload, not only the changed properties.

EDIT 2

Regarding the PATCH problem: it seems that GitHub API actually allows partial updates with POST as well because of the mentioned above possible PATCH incompatibility with some clients. So POST in addition to its normal duties (resource creation) gets the partial update capabilities when given resource ID (link -> section HTTP Verbs):

PATCH - Used for updating resources with partial JSON data. For instance, an Issue resource has title and body attributes. A PATCH request may accept one or more of the attributes to update the resource. PATCH is a relatively new and uncommon HTTP verb, so resource endpoints also accept POST requests.

Community
  • 1
  • 1
jannis
  • 4,843
  • 1
  • 23
  • 53
  • Jannis, thanks for taking the time to answer my question so extensively. The asynchronous approach with a 202 Accepted seems suitable for my situation. If in the future I'd like to make it possible for certain (automated) processes to edit the resource without dual control, the REST service can decide whether to return an 202 or 201 I guess. As for the approval itself, I've also seen implementations which model a 'decision' subresource on the proposal resource. A request can be verified via a POST on /userprofiles/{id}/changeproposal/decision – user1298232 Mar 29 '16 at 07:46
  • I don't like the `/userprofiles/{id}/changeproposal` approach because in case of creation it would suggest that the resource already exists and cause confusion: `/userprofiles/{id}/changeproposal/decision` would return the proposal resource, but what would `/userprofiles/{id}` return when the proposal hasn't been approved yet? 403? 404? 418:)? Unnecessarily complicated. – jannis Mar 29 '16 at 08:09
  • And what about adding a new collection? It would require you to implement the `changeproposal` subresource for every new collection in your API (an action is required even if it's a simple redirection). These are the two cases I can think of off the top of my head, but I'm almost sure that you'd get into more problems once you get into the implementation. This is why I'd rather have a separate root resource for proposals. IMHO it's simpler, more natural, flexible and straightforward for the consumers. – jannis Mar 29 '16 at 08:09