API: Capabilties - QutEcoacoustics/baw-server GitHub Wiki

API: Capabilities

DRAFT PROPOSAL: NOT IMPLEMENTED

Capabilities are a method for the server to define what actions are available to the current user.

The intention is to simply the logic required in client applications to make professional user interfaces. Encoding complex business and security rules is hard enough to do once in the server application but to make an effective user interface, those rules need to be reflected in the user interface. There are three options here:

  1. Maintain dual implementations of business and security rules in the client and server applications (and adjust both every time a change is needed)
    • this method gets worse for every new client implementation that follows
  2. Expose all actions in the client - even ones that will fail - and present error messages to the user when the action is invalid
    • terrible user experience
  3. Allow the server to list available actions that are valid at the time of the request

We currently have no real solution and thus by default are using option 1. Capabilities are an attempt to implement option 3.

Implementation details

Resources that support capabilities will expose a capabilities endpoint nested under the resource. The endpoint can apply to:

  • list responses
    • e.g. GET /projects/capabilities
    • Must be capabilities related to the type of the resource but not associated with any specific resource
  • or individual resources
    • e.g. GET /projects/1/capabilities
    • Must be related to the current resource

The capabilities endpoint will return a list of available actions for the current user.

Capabilities are always returned in a single-item standard API: Spec response body. They do not support:

  • paging
  • filtering
  • sorting
  • or the options pattern

Design choices multi-responses

There are no plans to add endpoints or support for returning capabilities for multiple resources at once. Previous experience has shown that most approaches are complex or seriously bloat the response payload (either in meta or as part of each object's fields).

Our API: Spec encourages chatty APIs instead.

Our current design enables fine-grained fetching for capabilities by adding endpoints for them. This again avoids payload bloat - especially when the client doesn't need capabilities for a resources.

This should also in theory improve performance on the sever as we do not need to hypothetically calculate all capabilities when returning a resource. This can left for a separate non-blocking request.

Standard capabilities

There are some standard capabilities:

  • create: the current user is allowed to POST /resource
  • update: the current user is allowed to PUT /resource/:id or PATCH /resource/:id
  • destroy: the current user is allowed to DELETE /resource/:id

There is no need for a read capability as:

  • for list responses, any items that are not readable by the current user will not be included in the list
  • for individual resources, any resources that are not readable by the current user will return a 40x error

Custom capabilities

Custom can be defined and returned on resources where it makes sense. Examples:

  • allow_original_download: is a setting on a project. Despite the field being available on the object it is not always determinable by the client what level of access a user will end up with. Thus it is useful to expose this as a capability on the projects resource.
  • suspend: is a custom capability on a job. It is only available when the job is in a running state. It is not available when the job is in a completed state. It makes sense to expose this as a capability.

Capabilities object definition

Capabilities are returned in a standard single item body response:

{
  "meta": {
    "status": 200,
    "message": "OK"
  },
  // @type: CapabilityMap
  "data": { /* ... */ }
}

The data will follow the following specification (TS notation):


// The name of the capability, always in snake_case
type CapabilityName = string;

interface CapabilityMap {
  [key: CapabilityName]: Capability;
}

interface Capability {
  // Whether or not a user is capable of performing the action
  can: boolean;
  // A code representing the reason why the user is not capable of performing the action
  // code is not human readable and is intended to be translated by the client application.
  // Only included when `can == false`
  code?: string;
  // A more detailed human readable message about why the user is not capable of performing the action
  // Only included when `can == false`
  details?: string;
  // A link to a resource this capability applies to. Relevant for "next-step" scenarios.
  // When relevant, included despite the value of `can`.
  // It will always be an absolute path.
  link?: string
}

Example payloads

For GET /projects/capabilities we would expect a response like:

{
  "meta": {
    "status": 200,
    "message": "OK"
  },
  "data": {
    "create": {
      "can": true
    },
  }
}

For GET /projects/1/capabilities we would expect a response like:

{
  "meta": {
    "status": 200,
    "message": "OK"
  },
  "data": {
    "update": {
      "can": true
    },
    "destroy": {
      "can": false,
      "code": "forbidden",
      "details": "You do not have permission to delete a project"
    },
    "allow_original_download": {
      "can": true
    }
  }
}

For GET /analysis_jobs/1/capabilities (using the API: Actions pattern):

{
  "meta": {
    "status": 200,
    "message": "OK"
  },
  "data": {
    "suspend": {
      "can": true,
      "link": "/analysis_jobs/1/suspend"
    },
    "resume": {
      "can": false,
      "code": "conflict",
      "details": "Cannot resume a job that is not suspended",
      "link": "/analysis_jobs/1/resume"
    },
    "retry": {
      "can": false,
      "code": "conflict",
      "details": "The job must be completed before it can be retried",
      "link": "/analysis_jobs/1/retry"
    },
    "amend": {
      "can": false,
      "code": "forbidden",
      "details": "You do not have permission to amend this job",
      "link": "/analysis_jobs/1/amend"
    }
  }
}

Historical notes

The previous spec (below) has multiple flaws: https://github.com/QutEcoacoustics/baw-server/issues/561

Implementation details

Requesting capabilities

OPTIONS

Capabilities are theoretically supported by the HTTP OPTIONS verb. For any resource in the API a:

OPTIONS /some_resource

request should return a list of available HTTP verbs in the Allow header and a capabilities object (described below) in the > payload.

However: Browser based API requests automatically send OPTIONS pre-flight requests to satisfy CORS security requirements. The > wrinkle here is that CORS pre-flight requests are meant to [exclude user credentials](https://www.w3.org/TR/cors/> #cross-origin-request-with-preflight-0) and thus without authentication, most of our capabilities would be indeterminable.

GET

Instead, we will package capabilities into the meta object returned for ANY GET request (see the API: Spec document). This > also has the advantage of reducing the number of HTTP requests required.

Capabilities are not included by default in the meta object and must be enabled with either an include_capabilities > query string parameter or a BAW-Include-Capabilities HTTP header. TODO: default inclusion or exclusion of capabilities in the > meta header is going through a field trial.

Capabilities Object Definition

In a GET (or a #filter POST) request, a capabilities object is inserted in the meta object as so:

{
    "meta": {
        "status": 200,
        "message": "OK",
        "capabilities": { ... }
    },
    "data": []
}

The capabilities object itself is an object hash of action names with metadata as in:

{
  // standard
  "create": {
    "can": true
  },
  "update": {
    "can": false,
    "details": "You need to have created this resource to update it",
    "message": "forbidden"
  },
  "destroy": {
    "can": false,
    "details": "You do not have permission to delete this resource",
    "message": "method_not_allowed"
  },
  // non-standard (can vary by resource)
  "pause": {
    "can": false,
    "details": "Pausing is not yet implemented",
    "message": "not_implemented"
  },
  "resume": {
    "can": false,
    "details": "Pausing is not yet implemented",
    "message": "not_implemented"
  },
  "retry": {
    "can": false,
    "details": "The job must be completed before it can be retried",
    "message": "unprocessable_entity"
  }
}

Standard actions

The standard actions map to the standard RAILS controller actions of create, update, and destroy and are invoked with the > appropriate HTTP verbs.

Non-standard actions

Several of our resources include state machines that allow transitions from one state to another on certain conditions. These can be > exposed as actions too. We assume the appropriate HTTP verb for updates (PUT or PATCH) are used.

Fields

  • can: Whether or not this action is currently allowed
  • details: (optional if can == true) A short reason that can be shown to the user if an action is not available. In some UI > patterns, it is useful to show unavailable actions rather than simply hiding them.
  • message: (optional if can == true, [should implement?]) A machine to machine tag indicating the broad categorical reason > for the state of the can attribute.
    • message was not implemented in V1 of capabilities
    • e.g. not_implemented, forbidden, unauthorized, precondition_failed