Tutorial 6 - Adding access control rules

Your event app currently allows you to simulate adding new user accounts and sign in as them, as well as create new events, edit existing events, and delete events, whose scope is restricted to a given signed-in user.

While your event app’s UI prevents one user from accessing another user’s event documents, blueprint can still be used to bypass the UI, by allowing anyone to make API calls to retrieve any event (as well as user) document. For example, API GET requests can still be made to either the /users or /users/{userid}/events endpoints to retrieve data, regardless of the user who was signed-in.

In this tutorial, you will tweak the existing blueprint to only allow authenticated users to access their own user document properties and event documents.

This tutorial takes around 20-30 minutes to complete.

Implementing access controls

Access controls are not implemented within the (client-side) app code itself, since these could easily be modified or overridden by custom JavaScript code, thereby permitting API calls to retrieve any data stored in your Datastore service.

Instead, access controls are implemented in the API specification (i.e. the api.yaml file). When your app is deployed to your Datastore service in Squiz DXP Console (DXP Console), your API specification (i.e. the blueprint which includes the API specification and defined access controls), is deployed and protected by the DXP Console Datastore service.

Define access control rules for all collections and documents

To start defining the appropriate access control rules for all collections and documents managed by your event app:

  1. In the event-app/api/ directory, open the api.yaml file.

  2. Locate each x-datastore-acl line in this file and update its public value with the Rule value for the appropriate API endpoint and Method combinations described in the table below. Ensure you have accounted for each Rule value in this table.

    Although a Rule value may be split over multiple lines in this table, ensure that its value is specified on a single line in the api.yaml file. Copying and pasting any Rule value (below) is handled on a single line, regardless of the number of lines it covers.

    x-datastore-acl is another Datastore extension directive, but this one applies to Datastore’s OpenAPI specification.

    Table 1. Updated x-datastore-acl rule values
    API endpoint Method Rule value Notes

    /users

    GET

    public

    This endpoint’s GET request remains public, so that anyone can attempt to retrieve this collection’s documents.


    When requesting all documents inside a collection, Datastore uses the x-datastore-acl rule defined in the document’s get request block to ensure that the user has access to the document.


    For a document to be included within a collection (from a GET request), the user (represented by the JWT’s userid value) would also need to satisfy the x-datastore-acl rule defined on the document’s get request block. Any documents to which the user does not satisfy the document’s x-datastore-acl rule are excluded from the collection.


    Although the event app does not use this path, making a request to it (based on the x-datastore-acl rule of its document endpoint in the next row below) will only ever return the currently authenticated user’s user document, or an empty collection if a user is not authenticated.

    /users/{userid}

    GET

    jwt.userid === document.userid

    This endpoint’s GET request only allows the request if the userid value in its JWT matches the userid property of the user document.


    From the users.json schema utilized by this endpoint as defined in tutorial 5, which in turn utilizes the user.json schema (also defined in tutorial 5), the userid property uses the x-datastore-source JSON schema Datastore extension property value of document.$id.


    Therefore, this rule ensures that the userid in the app’s JWT object matches the unique ID of the app’s user document, represented by {userid} in the URL path. The same result could also be achieved by specifying the rule as
    jwt.userid === document.$id.


    Once a user has been successfully authenticated to the authentication service (e.g. Matrix) registered with your Datastore service, the JWT representing this user can then be sent in the request as part of the payload to the Datastore service (via the JS SDK). For the request to succeed, this JWT received in the request by the Datastore service, must match that of the the JWT which Datastore also retrieves directly from this the authentication service. Therefore, this rule ensures that the user must be logged in to the registered authentication service, with the same user account represented by the JWT sent in the request.

    PUT

    jwt.userid === document.userid

    The same endpoint’s PUT request uses the same rule as its GET request. However, this rule only allows a new user document to be created when the userid value in its JWT matches the userid value for the user document being created.


    Following on from the additional explanations above, this rule ensures that to create a new user document (with the revised API specification), the JWT must represent the same user, who must already be logged in to the registered authentication service. This is why the JS SDK simulator function call in callout 4 of the createUser() function description in tutorial 5 is made to retrieve the JWT (from the simulated Datastore service) of the newly 'logged in' user, just before their user document is created in Datastore.


    This rule prevents a user from creating a user document as anyone else except themselves, and prevents one user from overriding another user’s account.

    /users/{userid}/events

    GET

    jwt.userid === parent.userid

    This rule only allows the request if the userid value in its JWT matches the {userid} component’s value (for the user document) in the request’s URL. This component of the URL is the parent of this request URL’s endpoint.

    POST

    /users/{userid}/events/{eventid}

    GET

    jwt.userid === parent.userid

    This rule only allows the request if the userid value in its JWT matches the {userid} component’s value (for the user document) in the request’s URL.


    Although this component of the URL is theoretically the grandparent of this request URL’s endpoint, the parent. syntax in the rule value only applies to the immediate ancestral URL components that contain properties. Since collections themselves do not contain properties, Datastore is aware of this and ignores any collection in the URL path.

    PATCH

    DELETE

Test the app with your access control changes

There are no changes to your event app’s index.html, main.js and login.js files between the last tutorial and this one.

  1. Test the updated app as you did in tutorial 5, and you should notice no change in app functionality.

    However, the access control rules you implemented above should now restrict a user’s access to data in your Datastore service, at the API level.

  2. Ensure you have created at least two users before continuing.

Test your event app’s access control changes to the API

  1. Log in through your event app as one of the users you created above.

  2. Access your browser’s Console feature:

    • In Google Chrome, you can access this through More tools  Developer tools  Console,

    • In Firefox, through Web Developer  Web Console,

  3. In your browser’s Console feature, specify the following command to retrieve all user account documents you have permission to access (i.e. only your own):

    datastore.collection('users').get();

    You should receive a response from the Datastore service similar to:

    Promise {<pending>}
      [[PromiseValue]]: Array(1)
        0:
          name: "John Smith"
          userid: "jsmith"
        length: 1

    which shows you property values of your logged in user, where John Smith and jsmith is an example name and username (i.e. userid).

    In Firefox, if the 'promise' results in a pending state (and does not return the Datastore service’s response to the console output), you may need to specify this command as:

    datastore.collection('users').get().then(result => { return console.log(result) });

    Now test what happens when you attempt to retrieve the event document of another user who is currently not logged in through your event app.

  4. While still logged in as your initial user (e.g. jsmith), in your browser’s Console feature, specify the following command to retrieve all event documents of a different user:

    datastore.collection('users').doc('jbloggs').collection('events').get();

    where jbloggs is the example username for another user you created.

    You should receive responses from the Datastore service similar to:

    GET http://0.0.0.0:8001/abcd1234/users/jbloggs/events 403 (Forbidden)
    
    Promise {<pending>}
      [[PromiseStatus]]: "rejected"
      [[PromiseValue]]: Object
        title: "Forbidden"
        status: 403
        debug: "ACL expression failed: jwt.userid === parent.userid"

    which indicates that as the user you are currently logged in with (e.g. jsmith), you do not have permission to access the events of the user jbloggs.

    In Firefox, if the 'promise' still results in a pending state or you receive an uncaught exception: Object error, specify this command as:

    datastore.collection('users').doc('jbloggs').collection('events').get().then(result => { return console.log(result) }).catch((e) => { return console.log(e) });

Reusing access control rules

Often, multiple API endpoints need to use the same access control rules, which results in the same rule value needing to be specified in each of these API endpoint/method definitions throughout the API specification. Duplicating rule values throughout an API specification can lead to problems in an app when a given rule value needs to be updated on a set of API endpoints, and one or a few are accidentally missed.

Such a scenario can leave the API in an inconsistent and possibly insecure state. Therefore, to avoid these issues, reusable rule values can be defined in a global components section of the API specification and referenced in the appropriate x-datastore-acl directives throughout the remainder of the API specification.

Implement reusable access control rules

To define reusable rule values in your API specification:

  1. In the event-app/api/ directory, open the api.yaml file.

  2. Copy YAML snippet (below), and paste it to the end of your api.yaml file, ensuring that components: is not indented.

    components:
      x-datastore-acl:
        mustBeCurrentUser: jwt.userid === document.userid
        mustBeEventOwner: jwt.userid === parent.userid
    Each reusable rule should have an obvious and descriptive property name, where this name reflects the purpose of the rule and is less likely to change than the rule’s value itself. By convention, camelCase is used for these property names.
  3. Replace each x-datastore-acl value in your api.yaml file again with the appropriate Reusable rule value in the following table.

    Table 2. Update your x-datastore-acl rule values with these reusable values
    API endpoint Method Reusable rule value

    /users/{userid}

    GET

    $ref: '#/components/x-datastore-acl/mustBeCurrentUser'

    PUT

    /users/{userid}/events

    GET

    $ref: '#/components/x-datastore-acl/mustBeEventOwner'

    POST

    /users/{userid}/events{eventid}

    GET

    $ref: '#/components/x-datastore-acl/mustBeEventOwner'

    PATCH

    DELETE

    A reusable rule value is referenced using the syntax:

    $ref: '#/components/x-datastore-acl/<reusable-rule-property-name>'

    and when specifying this syntax in your API specification, the resuable rule must be specified on its own line (immediately below x-datastore-acl) and indented one level in accordance with the rest of the YAML file (e.g. two spaces):

      /users/{userid}:
        parameters:
          ...
        get:
          description: Get a user profile by ID
          x-datastore-acl:
            $ref: '#/components/x-datastore-acl/mustBeCurrentUser'
          responses:
            '200':
              ...
  4. Re-test your event app’s access controls using these new reusable rule values.

    The browser console behaviour observed previously should still remain the same.


Now that you understand how to implement access control rules into your API endpoints to prevent unauthorized access to data in your Datastore service at the API level, you can now extend the functionality of your event app to sort event documents according to an event document’s property values.