REST Design for different actions

Let's say we have an object called Schedule . For simplicity, let's say the Timesheet object has three properties (TimesheetID, Status, and Hours).

We are currently allowing Timesheet object owners to submit their schedules through this endpoint -

   POST: /Users/{UserID}/Timesheets/{TimesheetID}

      

{UserID} is currently the owner of the Timesheet object . With {TimesheetID} I can also track the owner of the timesheet in the database.

OK, now here's the question -

We would now like their supervisors to be able to update the Schedule employee objects for this report (for example, approve / reject schedules ) change status or override hours).

However, there are different levels of manager permission. Some managers are allowed to update the status and some of them are allowed to update the status and hours.

Do I have to reuse the existing endpoint for both regular users and managers updates (I prefer)? Or should I create a different endpoint for each of the "Manager Level"?

If I hope to reuse an existing endpoint for all user views and manager updates, how do I handle errors like if the manager is configured to update state only, but the TimeSheet object sent to the REST service has changed the status and clock. Should I respond with a 403 detailing the error to inform the manager that you cannot change the clock property or update the status and ignore the clock?

+3


source to share


3 answers


Yes, go with 403 Forbidden

. I am matching the scenario that you are describing. RFC 7231 says:

The 403 (Forbidden) status code indicates that the server understood the request but refuses to authorize it. The server that wants to publish why the request was denied may be due to the response payload (if any).



Alternatively, the server can do whatever the current user is allowed to do and ignore everything else. If this is a good idea depends on your scenario. I prefer to deny the entire request and return 403 Forbidden

.

+1


source


If you follow the HATEOAS constraint , the ScheduleGET

resource will provide hyperlinks (links and forms) that can currently be used to interact with it. While this can be done in a number of ways that provide the smallest form of relationship, will include their actual parameters in the form.

In your example, I would include two manager forms with the same endpoint /Users/{UserID}/Timesheets/{TimesheetID}

. The first of them will have the status of a required field, the second - status (status can be optional) and hours (hours will be required).

You can then either respond to the second form with with 403 Forbidden

if the sender is not allowed to send hours. Or, alternatively, you can filter the forms included in GET

to display only those forms that the user can submit.

Update

For example, and GET

on /Users/1234/Timesheets/24

can currently create

<Timesheet>
    <TimesheetID>24</TimesheetID>
    <Status>Submitted</Status>
    <Hours>40</Hours>
</Timesheet>

      

To apply the HATEOAS constraint, we need to add hypermedia controls. We'll ignore the URLs for now because they are implementation details. This might give us something like

<Timesheet>
    <TimesheetID>24</TimesheetID>
    <Status>Submitted</Status>
    <Hours>40</Hours>
    <link rel="self" href="{{selfUrl}}"/>
    <form id="approve" action="{{approveUrl}}" method="PUT">
        <Status cardinality="required">
            <option value="Approve"/>
        </Status>
    </form>
    <form id="reject" action="{{rejectUrl}}" method="PUT">
        <Status cardinality="required">
            <option value="Reject"/>
        </Status>
    </form>
    <form id="update" action="{{updateUrl}}" method="PUT">
        <Status cardinality="required">
            <option value="Approve"/>
            <option value="Reject"/>
        </Status>
        <Hours type="decimal" cardinality="required"/>
    </form>

    ... there might be other forms too, like ...

    <form id="cancel" action="{{cancelUrl}}" method="DELETE"/>
</Timesheet>

      

What forms do (and how to recognize a form) is what is documented in the media type. For example:

The form cancel

on the schedule resource cancels the schedule, updating its status "Canceled". Once the schedule is canceled, it will no longer be possible to update the approval or reject the schedule.

Also in the media type, you will document the properties of the resources. eg.

The Timesheet resource has the following properties:

  • TimesheetID Unique identifier for the timesheet
  • Status The current state of the schedule. Status may include
    • Submitted Schedule drawn up but not approved
    • Approved Schedule made
    • Rejected Schedule was rejected
    • Canceled Cancel schedule
  • Hours The number of hours (decimal) recorded for the schedule

While this may be indicated by the schema, it should be avoided as it could result in resources being too complex later on. For example, you might decide to add a "WeekEnding" date property. Existing callers shouldn't worry about the new property, which is fine if they're just pulling out the data they're interested in. However, if you specify the schema without thinking about extending, then adding properties can cause validation errors in callers when adding properties.



Now, from the point of view of who can do what we have several options. One option is to simply turn on all the controls and respond with help 403

to any requests made by an unauthorized caller. Another option is to filter the controls so that they can only see the ones they can call. for example for a manager who can approve / reject they can get the following response

<Timesheet>
    <TimesheetID>24</TimesheetID>
    <Status>Submitted</Status>
    <Hours>40</Hours>
    <link rel="self" href="{{selfUrl}}"/>
    <form id="approve" action="{{approveUrl}}" method="PUT">
        <Status cardinality="required">
            <option value="Approve"/>
        </Status>
    </form>
    <form id="reject" action="{{rejectUrl}}" method="PUT">
        <Status cardinality="required">
            <option value="Reject"/>
        </Status>
    </form>
</Timesheet>

      

Whereas a manager who can update the watch can receive

<Timesheet>
    <TimesheetID>24</TimesheetID>
    <Status>Submitted</Status>
    <Hours>40</Hours>
    <link rel="self" href="{{selfUrl}}"/>
    <form id="approve" action="{{approveUrl}}" method="PUT">
        <Status cardinality="required">
            <option value="Approve"/>
        </Status>
    </form>
    <form id="reject" action="{{rejectUrl}}" method="PUT">
        <Status cardinality="required">
            <option value="Reject"/>
        </Status>
    </form>
    <form id="update" action="{{updateUrl}}" method="PUT">
        <Status cardinality="required">
            <option value="Approve"/>
            <option value="Reject"/>
        </Status>
        <Hours type="decimal" cardinality="required"/>
    </form>
</Timesheet>

      

Alternatively, you can enable all forms for all users, but add an indicator that they are not allowed to call it. for example for a manager who cannot update the clock:

<Timesheet>
    <TimesheetID>24</TimesheetID>
    <Status>Submitted</Status>
    <Hours>40</Hours>
    <link rel="self" href="{{selfUrl}}"/>
    <form id="approve" action="{{approveUrl}}" method="PUT">
        <Status cardinality="required">
            <option value="Approve"/>
        </Status>
    </form>
    <form id="reject" action="{{rejectUrl}}" method="PUT">
        <Status cardinality="required">
            <option value="Reject"/>
        </Status>
    </form>
    <form id="update" action="{{updateUrl}}" method="PUT" authorised="false">
        <Status cardinality="required">
            <option value="Approve"/>
            <option value="Reject"/>
        </Status>
        <Hours type="decimal" cardinality="required"/>
    </form>
</Timesheet>

      

I prefer this later approach as you don't get support for your API and the developers complain that a specific form doesn't exist. Either way (enabled or filtered), if the caller calls a form that they are not allowed, you will still respond 403

.

Disconnected from the topic, but for completeness, HATEOAS really comes to the fore because it passes a valid set of hypermedia controls based on the current state of the resource. for example when a schedule is canceled it is no longer valid for approval / rejection or renewal, so GET

the canceled schedule may revert

<Timesheet>
    <TimesheetID>24</TimesheetID>
    <Status>Cancelled</Status>
    <Hours>40</Hours>
    <link rel="self" href="{{selfUrl}}"/>
</Timesheet>

      

This clearly informs the caller that no other activity is allowed on that particular schedule.

The other thing you noticed is that we haven't provided any or URLs yet. They can all be the same (eg /Users/{UserID}/Timesheets/{TimesheetID}

), or they can be different (eg selfUrl = /Users/{UserID}/Timesheets/{TimesheetID}

, updateURL = /Users/{UserID}/Timesheets/{TimesheetID}/update

, etc.).

Ultimately the caller doesn't care as it will use whatever is in the form / link. This gives you a lot of flexibility as you can change them to suit your implementation needs. For example, if you are using Command Request Response Segmentation (CQRS), it might make sense to send requests GET

to //readonly.myserver.com/Users/{UserID}/Timesheets/{TimesheetID}

and POST

, PUT

and DELETE

requests //readwrite.myserver.com/Users/{UserID}/Timesheets/{TimesheetID}

.

+3


source


1) You are using POST to create, so you can use PUT on the same endpoint to update the data (providing new data in the content of the request). To restrict / register who updates the data, you can pass their user / manager id as a query parameter or inside the body

2) 403 Forbidden sound is better to make the user (manager) more understandable what happened, instead of letting him think that the data was updated correctly, but it was only partially updated in reality

+1


source







All Articles