Setting up access controls for properties

In this tutorial, we'll see how to use data policies and roles to set up scope-specific access controls for properties in LUSID.

Let's imagine we have a person entity that is configured with the properties Person/Details/Country and Person/SensitiveDetails/Address. We want to set up access controls that:

  • Allow most users to view Person/Details/Country and other properties within the Person/Details/* scope.

  • Allow a small number of privileged users to view Person/SensitiveDetails/Address and other properties within the Person/SensitiveDetails/* scope in addition to properties in the Person/Details/* scope.

To do this, we need to create and assign two different roles to our users, one role containing a feature and data policy that allows users to make requests to the GetPersonPropertyTimeSeries API and read Person/Details/* property definitions and property values data, and the other role containing a data policy that allows read-only access to Person/SensitiveDetails/* property definitions and property values data.

Note: To complete this tutorial, you must have suitable access control permissions. This can most easily be achieved by assigning your LUSID user the built-in lusid-administrator role, which  should already be the case if you are the domain owner. If you are informed you do not have a license to perform a certain operation, contact support.

Prerequisite: Setting up property definitions and person entities

Creating property definitions

For our scenario, we first need ensure we have the following property definitions:

  • Person/Details/Country

  • Person/SensitiveDetails/BankAccount

To create Person/Details/Country , we can call the CreatePropertyDefinition API, passing in the following:

curl -X POST "https://<your-domain>.lusid.com/api/api/propertydefinitions" 
  -H "Authorization: Bearer <your-API-access-token>" 
  -H "Content-Type: application/json-patch+json" 
  -d "{
    "domain": "Person",
    "scope": "Details",
    "code": "Country",
    "displayName": "Country trader operates in",
    "dataTypeId": {"scope": "system", "code": "string"},
    "lifeTime": "TimeVariant",
    "constraintStyle": "Property",
    "propertyDescription": "Country trader operates in"
}"

To create Person/SensitiveDetails/Address , we can call the CreatePropertyDefinition API again, passing in the following:

curl -X POST "https://<your-domain>.lusid.com/api/api/propertydefinitions"
-H "Authorization: Bearer <your-API-access-token>"
-H "Content-Type: application/json"
-d "{
  "domain": "Person",
  "scope": "SensitiveDetails",
  "code": "Address",
  "displayName": "Trader home address",
  "dataTypeId": {"scope": "system", "code": "string"},
  "lifeTime": "TimeVariant",
  "constraintStyle": "Property",
  "propertyDescription": "Home address of trader"
}"

Read more on creating a property type.

Creating a person entity

We also need to ensure we have a person entity that has been decorated with the above properties. For this tutorial, let's create a trader person entity to decorate with some properties. To do this:

  1. Create a property definition for a person entity identifier. The following call creates a property definition with a 3-stage property key of Person/Traders/TraderId:

    curl -X POST "https://<your-domain>.lusid.com/api/api/propertydefinitions"
    -H "Content-Type: application/json-patch+json"
    -H "Authorization: Bearer <your-API-access-token>"
    -d "{
        "domain": "Person",
        "scope": "Traders",
        "code": "TraderId",
        "valueRequired": true,
        "displayName": "Trader identifier for Person entities",
        "dataTypeId": { "scope": "system", "code": "string" },
        "lifeTime": "Perpetual",
        "constraintStyle": "Identifier",
    }"
  2. Call the UpsertPerson API to create a person entity. Note the labelValue you give the trader identifier must be unique among all traders represented in LUSID, in this case Trader5.

    curl -X POST "https://<your-domain>.lusid.com/api/api/persons"
    -H "Content-Type: application/json-patch+json"
    -H "Authorization: Bearer <your-API-access-token>"
    -d "{
        "identifiers": {
            "Person/Traders/TraderId": {
                "key": "Person/Traders/TraderId",
                "value": {
                    "labelValue": "Trader5"
                }
            }
        },
        "displayName": "John Doe",
        "description": "A portfolio manager and trader in US investments",
        "properties": {
            "Person/Details/Country": {
                "key": "Person/Details/Country",
                "value": {
                    "labelValue": "USA"
                }
            },
            "Person/SensitiveDetails/Address": {
                "key": "Person/SensitiveDetails/Address",
                "value": {
                    "labelValue": "123 New Lane, New York"
                },
                "effectiveFrom": "2016-07-01T00:00:00.0000000+00:00"
            }
    } }"

Read more on creating person entities.

Step 1: Creating a data and feature policy for unprivileged and privileged users

You can create policies to control access to the following actions for properties:

  • Read

  • Update

  • Delete

  • Any (all of the above)

For this tutorial, we will focus on controlling read access to various datapoints, but the same steps can be followed to control access for all of the actions above.

Creating a policy for a less privileged user

We first need to create a policy that allows a user to do all of the following, but not read Person/SensitiveDetails/*:

  • Grants access to the following LUSID features:

    • GetPerson API

    • GetPersonPropertyTimeSeries API

  • Grants read-only access to the following data points:

    • All person entities.

    • Property definitions and property values under the scope Person/Traders/*.

    • Property definitions and property values under the scope Person/Details/*.

To create a policy for all of the above:

  1. Sign into the LUSID web app and use the left-hand menu to navigate to Identity and Access > Policies:
     

  2. Select Create policy and choose JSON editor:
     

  3. Specify the following JSON, containing:

    • A code that uniquely identifies the policy; we'll call our policy allow-read-non-sensitive-person-properties.

    • A friendly description for the policy.

    • Any applications the policy relates to; for this tutorial, our policy relates only to LUSID.

    • Whether this policy will Allow or Deny access to the specified features and data under grant.

    • Within the selectors parameter, specify the following for our required feature policies:

      • Optionally, a name and description to help identify the selector.

      • An identifier for each feature, containing:

        • A scope value of LUSID.

        • A code value of api-persons-<endpoint> .

      • actions for the policy, including:

        • The scope value LUSID and entity value of feature.

        • The activity we want to grant access to; for this tutorial we want to allow policy holders to execute the endpoints.

    • For our required data policies, specify the following:

      • An identifier for each person entity or property we want to grant read-only access to, containing:

        • Optionally, a name and description to help identify the selector.

        • The domain for a property or personCode for a person entity.

        • The scope and code of the property or person entity.

    • A when parameter to further control access to these features and data, by applying a datetime the policy can activate from and, optionally, when it will deactivate and no longer provide any access.

    { "code": "allow-read-non-sensitive-person-properties",
      "description": "Allows read-only access to Person entity Details properties",
      "applications": [ "LUSID" ],
      "grant": "Allow",
      "selectors": [
          { "idSelectorDefinition": {
              "name": "Run GetPerson",
              "description": "Run the GetPerson API endpoint",
              "identifier": {
                "code": "api-persons-getperson",
                "scope": "LUSID"
              },
              "actions": [ {
                  "scope": "LUSID",
                  "activity": "Execute",
                  "entity": "Feature"
                } ]
            } },
          { "idSelectorDefinition": {
              "name": "Run GetPersonPropertyTimeSeries",
              "description": "Run the GetPersonPropertyTimeSeries API endpoint",
              "identifier": {
                "code": "api-persons-getpersonpropertytimeseries",
                "scope": "LUSID"
              },
              "actions": [ {
                  "scope": "LUSID",
                  "activity": "Execute",
                  "entity": "Feature"
                } ]
            } },
          { "idSelectorDefinition": {
              "name": "Read person entities",
              "description": "Read all person entities",
              "identifier": {
                "code": "*",
                "scope": "*",
                "personcode": "*"
              },
              "actions": [ {
                  "scope": "default",
                  "activity": "Read",
                  "entity": "Person"
                } ]
            } },
          { "idSelectorDefinition": {
              "name": "Read Person/Traders/*",
              "description": "Read Person/Traders/* property definition and values",
              "identifier": {
                "code": "*",
                "scope": "Traders",
                "domain": "Person"
              },
              "actions": [ {
                  "scope": "default",
                  "activity": "Read",
                  "entity": "PropertyDefinition"
                }, {
                  "scope": "default",
                  "activity": "Read",
                  "entity": "PropertyValue"
                } ] } },
          { "idSelectorDefinition": {
              "name": "Read Person/Details/*",
              "description": "Read Person/Details/* property definition and values",
              "identifier": {
                "code": "*",
                "scope": "Details",
                "domain": "Person"
              },
              "actions": [ {
                  "scope": "default",
                  "activity": "Read",
                  "entity": "PropertyDefinition"
                }, {
                  "scope": "default",
                  "activity": "Read",
                  "entity": "PropertyValue"
                } ] } } ],
        "when": { "activate": "2024-03-06T00:00:00.0000000+00:00",
          "deactivate": "9999-12-31T23:59:59.9999999+00:00" } }
  4. Select Create to save your new policy.

Creating a policy for a privileged user

We need to create a separate policy that allows a user to read property definitions and values under the scope Person/SensitiveDetails/*

To create this policy, we can follow the previous steps to create a policy via the LUSID web app, this time passing in the following: 

{   "code": "allow-read-sensitive-details-person-properties",
    "description": "Grants read access to Person/SensitiveDetails/* property definitions and values",
    "applications": [ "LUSID" ],
    "grant": "Allow",
    "selectors": [ {
        "idSelectorDefinition": {
          "identifier": {
            "code": "*",
            "scope": "SensitiveDetails",
            "domain": "Person"
          },
          "actions": [ {
              "scope": "default",
              "activity": "Read",
              "entity": "PropertyDefinition"
            }, {
              "scope": "default",
              "activity": "Read",
              "entity": "PropertyValue"
            } ] } } ],
    "when": { "activate": "2024-03-05T00:00:00.0000000+00:00",
      "deactivate": "9999-12-31T23:59:59.9999999+00:00" } }

Once created, we can view the policies in the Policies dashboard:

Step 2: Creating roles for unprivileged and privileged users

Now that we have created the required policies, we need to assign the policies to different roles which we can then assign to each user. For our tutorial, we need the following roles:

  • unprivileged-user assigned the policy allow-read-non-sensitive-person-properties.

  • privileged-user assigned the policies allow-read-non-sensitive-person-properties and allow-read-sensitive-details-person-properties.

The simplest way to create a role is via the LUSID web app; to do this:

  1. Sign into the LUSID web app and use the left-hand menu to navigate to Identity and Access > Roles:
     

  2. Let's first create our unprivileged role:

    1. Select Create role and specify the following:

      • A Code to uniquely identify the role; we can use unprivileged-user for our first role.

      • A friendly Description for the role.

      • An Activation date and, optionally, Deactivation date for the role. For our tutorial, we want the role to be active immediately, so we can specify the current date.

    2. Use the Policies > Choose dropdown to assign policies to the role. For our unprivileged role, we need to add the allow-read-non-sensitive-person-properties policy.
       

    3. Select Create to create the role.

  3. Now let's create our more privileged role:

    1. Select Create role and specify the following:

      • A Code to uniquely identify the role; we can use privileged-user for this role.

      • A friendly Description for the role.

      • An Activation date and, optionally, Deactivation date for the role. For our tutorial, we want the role to be active immediately, so we can specify the current date.

    2. Use the Policies > Choose dropdown to assign policies to the role. For our privileged role, we need to add both the allow-read-non-sensitive-person-properties and allow-read-sensitive-details-person-properties policies.
       

    3. Select Create to create the role.

Note: Depending on other existing roles and policies in your domain, you may need to set a precedence for each role to determine which policies take effect in the event of a conflict. Read more on setting precedence for a role here.

Step 3: Assigning roles to users and seeing the access controls in action

Once we have created the roles, we can assign them to our desired users via the LUSID web app to enforce who can view properties containing sensitive data. To do this:

  1. Sign into the LUSID web app use the left-hand menu to navigate to Identity and Access > Users.

  2. Locate a user to assign a role to and select Edit.
     

  3. Click Add roles and select the unprivileged-user role we made earlier.
     

  4. Click Save to add the role to the user.

  5. To create a privileged user, repeat steps 2-4, additionally assigning the privileged-user role to the user.
     

Once a user is assigned the unprivileged-user role, we can see the policy in action by having the user call the GetPersonPropertyTimeSeries API, passing in the following:

curl -X GET
  "https://<your-domain>.lusid.com/api/api/persons/Traders/TraderId/Trader5/properties/time-series?propertyKey=Person%2FDetails%2FCountry" 
  -H "Authorization: Bearer <your-API-access-token>"

Part of the response is as follows; the unprivileged user can access the non-sensitive data:

{
  "values": [
    {
      "value": {
        "labelValue": "USA"
      },
      ...

However, if the unprivileged user attempts to view properties under the Person/SensitiveDetails/* scope by passing in the following:

curl -X GET
  "https://<your-domain>.lusid.com/api/api/persons/Traders/TraderId/Trader5/properties/time-series?propertyKey=Person%2FSensitiveDetails%2FAddress" 
  -H "Authorization: Bearer <your-API-access-token>"

The request fails with part of the response as follows:

{
  "name": "AccessDenied",
  "title": "Access cannot be granted",
  "status": 403,
  "detail": "Access to perform the requested action cannot be granted due to insufficient privileges. Please contact your organisation's administrator.",
  "instance": "https://<your-domain>.lusid.com/app/insights/logs/0HN1UO1IR6S4U:00000131",
  ...
}

We can follow the instance URL and look under the Access logs tab to understand which policy is required to gain access to such data:

If our privileged user attempts the same API call, the user can read the sensitive data; part of the response is as follows:

{
  "values": [
    {
      "value": {
        "labelValue": "123 New Lane, New York"
      },
     ...