New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build ACL layer #396

Closed
eduardoboucas opened this Issue Mar 19, 2018 · 2 comments

Comments

Projects
None yet
2 participants
@eduardoboucas
Copy link
Member

eduardoboucas commented Mar 19, 2018

From internal spec document:


Introduction

The single most frequent question I get when I present our platform to developers at meetups and conferences is “how can I use API with my front-end application?”. The answer usually references Publish, a production, Preact-based web app that interacts with API, although not directly.

Current approach: the middleman

Publish users don’t actually interact with API directly. When they sign in with their email address and password, they authenticate with the Publish backend app, which in its turn will communicate with API to understand if the credentials supplied correspond to a valid user. It will do so using a clientId/secret pair, with a full set of permissions, which is never disclosed to the user.

With these credentials, it will send the email address/password pair typed by the user to API to understand whether they match a valid user. This all happens in a hook, containing custom logic that will respond to different types of request (e.g. create user, validate user, sign out) and interact with a users collection created for the effect.

We do this for two reasons:

  • API doesn’t provide the fine-grained level of authorisation that we would expect on a typical CMS. It only goes as far as specifying that a certain client either has access to all the collections or to a specific list, but anything more advanced like roles, hierarchies and document-level permissions is handled in Publish.

  • It allows us to create sessions. When someone signs in with Publish, the back-end app will store a session and therefore subsequent interactions with the app will be automatically authenticated. If we were interacting with API directly, we could exchange a clientId/secret pair for a bearer token, but storing it on the client for use in subsequent requests would be difficult.

But it comes with big disadvantages:

  • You can’t easily build an app that interacts with API unless you have a back-end. Ideally, you’d be able to spin up a static HTML page with some JavaScript and use API to feed it. Forcing people to have a middleman app to handle ACL is a deal breaker to most people.

  • Something as fundamental as users, roles and permissions are an important part of the business logic, and therefore should be at the core of the data layer. By keeping this in Publish, it means that creating a different CMS (for example, for a non-web flow) involves rewriting all the ACL layer, leading not only to a huge duplication of effort but also potential problems caused by discrepancies between systems.

  • It’s brittle. The ACL logic currently lives in hooks, so projects using it have another barrier to updating their version of API, because they need to ensure their bespoke hooks are compatible with any changes introduced in core.

This document proposes an update to API with the aim to fix the issues above, in two steps.

Step 1: Build ACL into API

The first step would be to extend the authorisation layer of API to include things like:

  • Roles: named sets of permissions, which define the operations that a group of users can and cannot perform. These should be possible to create, update and delete via a series of REST endpoints

  • Hierarchies

    • Roles can inherit from other roles (e.g. administrator inherits from editor)
  • Users can belong to a role and optionally override certain aspects of it (e.g. Mark is a special type of sports editor that also has access to a lifestyle collection)

  • Document-level permissions

    • Ability to define a group of roles and/or users that can have read/write access to a certain document
    • Ability to limit clients to only read/write documents created/owned by themselves
  • Introduce the ability to create new clients via REST endpoints. At the moment, the only way to create a client is through CLI, but we’d need to empower users to create other users (providing they have the right permissions for it, of course).

The full set of roles/permissions would be stored against every new client created, and that information would be sent back to users whenever they request a bearer token for their respective clients.

POST /token

{
    "accessToken": "fff35720-f9b5-4e6a-aa62-3f9cf0103f92",
    "tokenType": "Bearer",
    "expiresIn": 1800000,
    "permissions": {
      "role": "editor",
      "collections": {
        "cars": {
          "read": true,
          "write": true
        },
        "boats": {
          "read": true,
          "write": true
        },
        "lifestyle": {
          "read": true,
          "write": false
        }
      }
    }
}

(Example of a response to the /token endpoint, listing the user’s permissions)

Step 2: Easier front-end authentication flow

The problem

In version 1.4.0, we added support for CORS, meaning that users can – to some degree – interact with API from a front-end application. But it’s still a challenge to fully integrate a client-side JavaScript application with API, especially around authentication.

Each end user can get their own clientId/secret pair, which they can then exchange for a bearer token at sign in stage using the application interface (e.g. using an Ajax request). But what should the application do with that bearer token?

Currently, all subsequent requests to API must include that token in the Authorization header, so the application must store it somehow on the client and inject it into every request. A single page application can do this by keeping the token in memory, but that won’t survive a full page refresh. A number of techniques like local storage could be used, but there are always compatibility and browser-specific issues involved.

A proposed solution

I propose that each client can have an optional allowedOrigins property which, when configured, defines a list of domains from where front-end authentication is expected. This flow would allow the following:

  1. User signs in into front-end application
  2. FE app sends the user credentials in an Ajax request to API (/token endpoint)
  3. If the origin matches an allowed domain AND the client ID/secret pair match a valid user, a new bearer token is returned in the body of the request but also put in a cookie
  4. Any subsequent requests made from the FE app will contain the bearer token in the cookie, which will be checked by API if the Authorization header is not present. This will survive full page reloads

Worth noting that:

  • The cookie will be set with the Expires property corresponding to the token TTL, so that the consumer application knows at all time when the user session will expire

  • The list of domains is used by the Access-Control-Allow-Origin and Access-Control-Allow-Credentials HTTP headers to only allow this flow from trusted origins

  • The cookie can be secure and encrypted, but ultimately it will never contain any information that could compromise the system. It’s just a convenience method for storing a bearer token obtain from API in such a way that the browser takes care of authenticating with API automatically. For all purposes, this is just a session cookie.

Conclusion

I have a proof of concept with these changes added to API, along with a dead simple web app that displays a list of data when the user is signed in and a login form otherwise. With just a few lines of code, the user session is preserved in between page reloads for as long as the bearer token is valid.

In the extreme, this would mean that an application like Publish could be built without a backend, it could be as simple as a JS file kept in a centralised CDN that is included in a HTML page via a script tag, meaning that upgrading is as simple as changing the URL to point to a new version.

It also means that any other consumer app gets the same users, roles and permissions, enforcing consistency across the entire system.

@mingard

This comment has been minimized.

Copy link
Member

mingard commented Mar 19, 2018

Spec looks great!

Can we extend it to cover filtering too? Ideally we'd be able to apply filters to collections for specific users, such as only allowing the user to edit documents that have a particular field value.

Another useful extension would be to restrict editing of content that was created by the user.

Both of these concepts are currently required in client projects.

@eduardoboucas

This comment has been minimized.

Copy link
Member Author

eduardoboucas commented Mar 19, 2018

Yes and yes, both super useful additions. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment