NAV Navbar
  • Creating a Read-Only User
  • Creating a Read-Only User

    This tutorial describes how to create a user with read-only permissions in the Form3 API. As a starting point, we use an existing user (usually the sysadmin user) and copy its READ permissions into a new user.

    When you first gain access to the Form3 API as a new customer, you will receive an admin user login that has read and write permissions for all resources you can use. However, you may not want all individuals in your organisation to have this type of access. In fact, we recommend using a read-only role for all actions not requiring write access.

    This tutorial starts with a quick intro about user permission management in Form3. Then, we'll go through the steps required to create a read-only user. You can also find a ready-to-run Python script to accomplish this at the end of the tutorial.

    User permission management in Form3

    Permission management on the Form3 platform is divided into multiple components. At the highest level, each customer is represented by an organisation. Each organisation has at least one user. Each user is assigned a role which is used to group permissions together. A role can be assigned to multiple users and a user can also have multiple roles.

    A permission is represented by an Access Control Entry (ACE). An ACE defines a permission based on three key parameters:

    User permissions structure

    A new ACE can be created using the add ACE call. The following ACE has just been created for a role with the ID 0614c823-bceb-4c68-97a8-05ff57b74d6a and grants it read access to Payment resources:

    Read payments ACE

    HTTP/1.1 201 Created
    Content-Type: application/json
    
    {
        "type": "Ace",
        "id": "30bdbf9f-3078-48b6-b8ff-995d2350b12e",
        "version": 0,
        "organisation_id": "a475b24c-d1ad-41c0-a28f-5f086ebe866c",
        "attributes": {
            "role_id": "0614c823-bceb-4c68-97a8-05ff57b74d6a",
            "action": "READ",
            "record_type": "payments"
        }
    }
    

    Step-by-step guide

    The following steps are required to create a user with read permissions based on an existing user with root-access:

    All these steps can be achieved using the Form3 API. The following sections describe each step in more detail. You can find a complete Python script for this process at the end of this tutorial.

    Note that in order to create a new user and role and assign permissions to them, your current user needs to have read and write permissions for the User, Role, and ACE record types. Usually only your sysadmin user has these permissions.

    Authenticate with the Form3 API

    To be able to interact with our API, you first have to authenticate yourself using the OAuth 2 mechanism. To log in, you need a client_id and a client_secret. In return, you'll receive an access token which has to be included in the header of every further API call.

    See here for more information about how to obtain an access token.

    Retrieve permissions from the sysadmin user

    To find out which permissions to give your read-only user, first retrieve all READ permissions from your sysadmin user. To be able to do that, you'll need that user's user_id.

    If you don't know the user ID, run a list call for all users: GET /v1/security/users. This call will return a list of all users in the system. Identify the user by its username or email address and write down the value of the id attribute of the User resource.

    Now use the list ACEs call for to get all read permissions that are associated with your user: GET /v1/security/users/{user_id}/aces?filter[action]=READ. Save the list of ACEs and set them aside for later.

    The list ACEs call will return a list of ACEs like this:

    HTTP/1.1 200 OK
    Content-Type: application/vnd.api+json
    
    {
      "data": [
        {
          "type": "ace",
          "id": "5110b52d-6081-4ce0-86e8-cddccb1f6bfb",
          "organisation_id": "b0a7259c-bf91-4368-8769-d90d2652c91d",
          "attributes": {
            "role_id": "110d6286-1393-492c-be80-9f5924b9cd7c",
            "action": "READ",
            "record_type": "payments"
          }
        },
        {
          "type": "ace",
          "id": "6e89b6fc-0de6-45d7-91ee-bb1ac596f407",
          "organisation_id": "b0a7259c-bf91-4368-8769-d90d2652c91d",
          "attributes": {
            "role_id": "110d6286-1393-492c-be80-9f5924b9cd7c",
            "action": "READ",
            "record_type": "payment_submissions"
          }
        },
        {
          "type": "ace",
          "id": "1885a640-2828-4f1a-8bd9-d55dc3f6368e",
          "organisation_id": "b0a7259c-bf91-4368-8769-d90d2652c91d",
          "attributes": {
            "role_id": "4278435e-41ad-4b97-b9de-04d1bcc4aecb",
            "action": "READ",
            "record_type": "returns"
          }
        },
        (...)
      ],
      "links": {
        "self": "https://api.form3.tech/v1/users/a1e818a5-a45b-4c88-810a-db241de62a1f/aces"
      }
    }
    

    Create a new user

    Now create a new user for your organisation using POST /v1/security/users. Choose a username in the username attribute and add a valid email address in the email attribute.

    You should receive a confirmation response similar to this one:

    HTTP/1.1 201 Created
    Content-Type: application/json
    
    {
      "data": {
        "type": "User",
        "id": "271253ab-2da7-426e-8080-39239025dacb",
        "version": 0,
        "organisation_id": "af8cd25a-0005-474d-81c6-3b87476e9dc1",
        "attributes": {
            "username" : "example-read-only-user",
            "email" : "example.read-only-user@form3.tech",
            "role_ids" : []
      },
      "links": {
        "self": "https://api.form3.tech/v1/users/271253ab-2da7-426e-8080-39239025dacb
      }
    }
    

    Fetch the client credentials

    To be able to use the new user, you need to generate a client ID and client secret for it. These values can be obtained by calling POST /v1/security/user/{user_id}/credentials. Note that each call to this endpoint generates a new set of credentials, invalidating any previous ones.

    When calling the endpoint, you should receive a response like this:

    {
        "data": {
            "client_id": "e468e304-352e-40a7-976c-bd7854abaee3",
            "client_secret": "0efb7211-2884-4047-a997-af63ac7e427a"
        },
        "links": {
            "self": "https://api.development.form3.tech/v1/security/users"
        }
    }
    


    Store the values of client_id and client_secret in a secure place, as they are very valuable and you will need them later to log into your read-only account.

    Create a new role with read-only permissions

    Next, create a new Role resource by calling POST /v1/security/roles. The only attribute you need to provide is a name for the role. The response will look like this:

    HTTP/1.1 201 Created
    Content-Type: application/json
    
    {
      "data": {
        "type": "roles",
        "id": "1b1de340-1603-4cc7-80bb-09c73d60788f",
        "version": 0,
        "organisation_id": "c29cb42d-fbb3-400c-bad4-aead17085063",
        "attributes": {
          "name": "read-only"
        }
      },
      "links": {
        "self": "https://api.form3.tech/v1/roles/1b1de340-1603-4cc7-80bb-09c73d60788f"
      }
    }
    

    Add read permissions to the new role

    Remember the list of read ACEs you extrated from your sysadmin user in the beginning? Now, each of these ACEs needs to be re-created for the new Role resource. This is done using the add ACE call of the Role resource: POST /v1/security/roles/{role_id}/aces. Note that role_id in both the URL and the body refers to the new role in this call.

    Below is an example of the body of the POST call:

    {  
      "data": {
        "type": "ace",
        "id": "0c5fb47f-feb4-42d0-ab21-ef84ec57839c",
        "version": 0,
        "organisation_id": "c29cb42d-fbb3-400c-bad4-aead17085063",
        "attributes": {
          "role_id": "1b1de340-1603-4cc7-80bb-09c73d60788f",
          "action": "READ",
          "record_type": "payments"
        }
      }
    }
    


    The list of read ACEs will most likely contain several dozen entries, making creating new ACEs a time-intensive task. We don't recommend doing this by hand, instead use the Python script provided at the end of this tutorial to automate all steps outlined on this page.

    Assign the role to the new user

    Finally, finish the user setup by assigning the role to your read-only user. This is done with POST /v1/security/users/{user_id}/roles/{role_id}, where user_id is the ID of the new read-only user and role_id the ID of the new read-only role. Note that this call does not require a body, as all information is contained in the URL parameters.

    Check the new user

    To check if everything went well, simply list all permissions that your new user has using the list ACEs call we used above: GET /v1/security/users/{user_id}/aces. All ACEs in the list should say READ as their action and all record types listed in the previous fetch call should be present.

    Code example

    Below is a ready-to-run Python script that contains all the steps discussed in this tutorial. Make sure to fill in the required information in the respective variables before running the script. You'll need your organisation ID and Form3 credentials (client_id and client_secret), as well as the user ID of your sysadmin user.

    The code example uses the requests package to access the Form3 API. The code is tested with Python 3.5, but most likely will also work with other Python versions.

    import math, requests, time, uuid, json
    from pprint import pprint
    
    ### Replace these variables with your own data! ###
    client_id = 'YOUR CLIENT ID HERE'
    client_secret = 'YOUR CLIENT SECRET HERE'
    organisation_id = 'YOUR ORGANISATION ID HERE'
    sysadmin_user_id = 'THE ID OF YOUR EXISTING ADMIN USER HERE'
    organisation_name = 'YOUR ORGANISATION NAME HERE'
    new_user_email = 'THE EMAIL ADDRESS FOR THE NEW USER HERE'
    
    base_url = 'https://api.test.form3.tech/v1'
    
    # Generate IDs
    new_user_id = uuid.uuid4()
    print("User ID: %s" % new_user_id)
    
    role_id = uuid.uuid4()
    print("Role ID: %s" % role_id)
    
    # Obtain authentication token
    print("Obtaining bearer token...")
    auth_payload = "grant_type=client_credentials"
    auth_url = '%s/oauth2/token' % base_url
    auth_headers = {'Content-Type': 'application/x-www-form-urlencoded'}
    auth_request = requests.auth.HTTPBasicAuth(client_id, client_secret)
    
    auth = requests.request('post', auth_url, data=auth_payload, auth=auth_request, headers=auth_headers)
    auth_token = auth.json().get('access_token')
    
    # Create a new user
    print("Creating new user %s-read-only..." % organisation_name)
    user_url =  "%s/security/users" % base_url
    user_payload = """
    {
        "data": {
            "id": "%s",
            "version": 0,
            "organisation_id": "%s",
            "attributes": {
                "email" : "%s",
                "username" : "%s-read-only"
            }
        }
    }
    """ % (new_user_id, organisation_id, new_user_email, organisation_name)
    
    user_headers = {
        'authorization': "bearer " + auth_token,
        'accept': "application/json",
        'content-type': "application/json",
        'cache-control': "no-cache"
        }
    
    new_user = requests.request("POST", user_url, data=user_payload, headers=user_headers)
    
    # Get credentials for new user
    print("Getting credentials for new user...")
    credentials_url =  "%s/security/users/%s/credentials" % (base_url, new_user_id)
    credentials_header = {
        'authorization': "bearer " + auth_token,
        'accept': "application/json",
        'content-type': "application/json",
        'cache-control': "no-cache"
        }
    
    credentials = requests.request("POST", credentials_url, headers=credentials_header)
    credentials_content = json.loads(credentials.text)
    print("New user's client_id: %s" % credentials_content['data']['client_id'])
    print("New user's client_secret: %s" % credentials_content['data']['client_secret'])
    
    # Create a new role
    print("Creating new role %s-read-only-role..." % organisation_name)
    role_url =  "%s/security/roles" % base_url
    role_payload = """
    {
        "data": {
            "id": "%s",
            "version": 0,
            "organisation_id": "%s",
            "attributes": {
                "name" : "%s-read-only"
            }
        }
    }
    """ % (role_id, organisation_id, organisation_name)
    
    role_headers = {
        'authorization': "bearer " + auth_token,
        'accept': "application/json",
        'content-type': "application/json",
        'cache-control': "no-cache"
        }
    
    new_role = requests.request("POST", role_url, data=role_payload, headers=role_headers)
    
    # Get all existing ACEs from the existing user
    print("Getting exisiting READ ACEs from user...")
    list_acl_url = "%s/security/users/%s/aces" % (base_url, sysadmin_user_id)
    list_acl_querystring = {"filter[action]":"READ"}
    list_acl_headers = {
        'Authorization': "bearer %s" % auth_token,
        'Accept': "application/vnd.api+json",
        'Cache-Control': "no-cache"
        } 
    
    list_acl = requests.request("GET", list_acl_url, headers=list_acl_headers, params=list_acl_querystring)
    read_acl = json.loads(list_acl.text)
    record_types = set([item['attributes']['record_type'] for item in read_acl['data']])
    print("Found %d record types." % len(record_types))
    
    # Add ACEs to new role
    ace_url =  "%s/security/roles/%s/aces" % (base_url, role_id)
    
    ace_headers = {
        'authorization': "bearer " + auth_token,
        'accept': "application/json",
        'content-type': "application/json",
        'cache-control': "no-cache"
        }
    
    for record_type in record_types:
        print("Adding ACE for record type %s..." % record_type)
        ace_payload = """
        {
            "data": {
                "id": "%s",
                "version": 0,
                "organisation_id": "%s",
                "attributes": {
                    "action":"READ",
                    "record_type":"%s",
                    "role_id": "%s"
                }
            }
        }
        """ % (uuid.uuid4(), organisation_id, record_type, role_id)
    
        ace = requests.request("POST", ace_url, data=ace_payload, headers=ace_headers)
    
    # Add role to user
    add_role_url =  "%s/security/users/%s/roles/%s" % (base_url, new_user_id, role_id)
    
    add_role_headers = {
        'authorization': "bearer " + auth_token,
        'accept': "application/json",
        'content-type': "application/json",
        'cache-control': "no-cache"
        }
    
    add_role = requests.request("POST", add_role_url, headers=add_role_headers)
    pprint(add_role.text)
    
    # List all ACEs for new user for verification
    list_new_acl_url = "%s/security/users/%s/aces" % (base_url, new_user_id)
    list_new_acl_headers = {
        'Authorization': "bearer %s" % auth_token,
        'Accept': "application/vnd.api+json",
        'Cache-Control': "no-cache"
        } 
    
    list_new_acl = requests.request("GET", list_new_acl_url, headers=list_new_acl_headers)
    read_new_acl = json.loads(list_new_acl.text)
    new_record_types = [item['attributes']['record_type'] for item in read_new_acl['data']]
    if len(new_record_types) == len(record_types):
        print("Success! New user has %d READ permissions." % len(new_record_types))
    else:
        print("Something went wrong. New user has %d READ permissions, should be %d" % (len(new_record_types), len(record_types)))