Back to all resources
KBAC KBAC Json

KBAC: Token-Scope-Gated Read/Write Permissions

Gate KBAC actions on the bearer token's scope claim. CAN_READ requires scope 'cars.read', CAN_WRITE requires scope 'cars.write'. The graph relationship grants the base capability; the token scope decides which verbs are exercisable on this request.

KBAC: Token-Scope-Gated Read/Write Permissions

This example demonstrates KBAC policies that take both the graph and the bearer token's scope claim into account.

Pattern:

- The condition.cypher establishes the relationship-based base permission (e.g., subject DRIVES resource).

- The condition.filter narrows the action by inspecting $token.scope with the CONTAINS operator:

- CAN_READ requires the scope claim to contain 'cars.read'.

- CAN_WRITE requires the scope claim to contain 'cars.write'.

- Two separate policies coexist; the runtime selects them by action.

Effect: a single user identity (Knight Rider, who DRIVES KITT) can be authorized for CAN_READ today but denied CAN_WRITE because the issued bearer token does not include the cars.write scope. Re-authenticate with a wider-scoped token to gain CAN_WRITE without changing any policy.

Use case

Scenario: A vehicle service portal issues bearer tokens with scopes that match what the user can actually do this session.

- Mechanic token: scope = "cars.read cars.write" -> can read AND modify car records.

- Customer token: scope = "cars.read" -> can read their own car record, cannot modify it.

Policies:

1. CAN_READ on Car when subject DRIVES car AND $token.scope contains 'cars.read'.

2. CAN_WRITE on Car when subject DRIVES car AND $token.scope contains 'cars.write'.

Calls:

- Knight Rider with cars.read token: CAN_READ KITT -> true; CAN_WRITE KITT -> false.

- Mechanic with full scopes: both -> true.

This is the cleanest place to put scope checks: in the policy condition, not in the client.

ikg

Requirements

Prerequisites:

- ServiceAccount credentials: For creating the policies.

- AppAgent credentials: For calling /access/v1/evaluation.

- A Token Introspect configuration so the API can verify the bearer token and extract its scope claim.

- Graph data with DRIVES relationships.

Required API access:

- POST /configs/v1/authorization-policies (one per action)

- POST /access/v1/evaluation (caller passes the user's bearer token in the Authorization header)

Steps

Step 1: Capture Graph Data

- Action: POST Person and Car nodes plus DRIVES relationships (re-use the authZEN dataset).

Step 2: Create the CAN_READ Policy

- Action: POST a policy whose condition.filter requires $token.scope to CONTAIN 'cars.read'.

Step 3: Create the CAN_WRITE Policy

- Action: POST a similar policy requiring $token.scope to CONTAIN 'cars.write'.

Step 4: Evaluate with a Narrow-Scope Token

- Action: POST /access/v1/evaluation with action CAN_READ, sending Authorization: Bearer <token> where the token's scope claim contains only cars.read.

- Result: authorized.

Step 5: Evaluate the Same Subject for CAN_WRITE

- Action: POST /access/v1/evaluation with action CAN_WRITE using the same narrow-scope token.

- Result: denied - the scope check fails even though DRIVES exists.

Step 1

Capture Person and Car nodes.

POST https://eu.api.indykite.com/capture/v1/nodes/Json
{
  "nodes": [
    {
      "external_id": "alice",
      "is_identity": true,
      "type": "Person",
      "properties": [
        {
          "type": "email",
          "value": "alice@email.com"
        },
        {
          "type": "given_name",
          "value": "Alice"
        },
        {
          "type": "last_name",
          "value": "Smith"
        }
      ]
    },
    {
      "external_id": "knightrider",
      "type": "Person",
      "is_identity": true,
      "properties": [
        {
          "type": "email",
          "value": "knightrider@demo.com"
        },
        {
          "type": "name",
          "value": "Michael Knight"
        }
      ]
    },
    {
      "external_id": "satchmo",
      "type": "Person",
      "is_identity": true,
      "properties": [
        {
          "type": "email",
          "value": "satchmo@demo.com"
        },
        {
          "type": "name",
          "value": "Louis Armstrong"
        }
      ]
    },
    {
      "external_id": "karel",
      "type": "Person",
      "is_identity": true,
      "properties": [
        {
          "type": "email",
          "value": "karel@demo.com"
        },
        {
          "type": "name",
          "value": "Karel Plihal"
        }
      ]
    },
    {
      "external_id": "kitt",
      "type": "Car",
      "is_identity": false,
      "properties": [
        {
          "type": "manufacturer",
          "value": "pontiac"
        },
        {
          "type": "model",
          "value": "Firebird"
        }
      ]
    },
    {
      "external_id": "cadillacv16",
      "type": "Car",
      "is_identity": false,
      "properties": [
        {
          "type": "manufacturer",
          "value": "Cadillac"
        },
        {
          "type": "model",
          "value": "V-16"
        }
      ]
    },
    {
      "external_id": "harmonika",
      "type": "Bus",
      "is_identity": false,
      "properties": [
        {
          "type": "manufacturer",
          "value": "Ikarus"
        },
        {
          "type": "model",
          "value": "280"
        }
      ]
    },
    {
      "external_id": "listek",
      "type": "Ticket",
      "is_identity": false
    },
    {
      "external_id": "airbook-xyz",
      "type": "Laptop",
      "is_identity": false
    }
  ]
}

Capture DRIVES relationships.

POST https://eu.api.indykite.com/capture/v1/relationships/Json
{
  "relationships": [
    {
      "source": {
        "external_id": "knightrider",
        "type": "Person"
      },
      "target": {
        "external_id": "kitt",
        "type": "Car"
      },
      "type": "DRIVES"
    },
    {
      "source": {
        "external_id": "satchmo",
        "type": "Person"
      },
      "target": {
        "external_id": "cadillacv16",
        "type": "Car"
      },
      "type": "DRIVES"
    },
    {
      "source": {
        "external_id": "karel",
        "type": "Person"
      },
      "target": {
        "external_id": "listek",
        "type": "Ticket"
      },
      "type": "HAS"
    },
    {
      "source": {
        "external_id": "listek",
        "type": "Ticket"
      },
      "target": {
        "external_id": "harmonika",
        "type": "Bus"
      },
      "type": "FOR"
    },
    {
      "source": {
        "external_id": "karel",
        "type": "Person"
      },
      "target": {
        "external_id": "airbook-xyz",
        "type": "Laptop"
      },
      "type": "OWNS"
    },
    {
      "source": {
        "external_id": "alice",
        "type": "Person"
      },
      "target": {
        "external_id": "airbook-xyz",
        "type": "Laptop"
      },
      "type": "OWNS"
    },
    {
      "source": {
        "external_id": "knightrider",
        "type": "Person"
      },
      "target": {
        "external_id": "kitt",
        "type": "Car"
      },
      "type": "OWNS"
    },
    {
      "source": {
        "external_id": "alice",
        "type": "Person"
      },
      "target": {
        "external_id": "cadillacv16",
        "type": "Car"
      },
      "type": "DRIVES"
    }
  ]
}

Step 2

Policy: CAN_READ on Car requires the bearer token's scope claim to contain 'cars.read'.

policy_read.jsonJson
{
  "meta": {
    "policy_version": "2.0-kbac"
  },
  "subject": {
    "type": "Person"
  },
  "actions": [
    "CAN_READ"
  ],
  "resource": {
    "type": "Car"
  },
  "condition": {
    "cypher": "MATCH (subject:Person)-[:DRIVES]->(resource:Car)",
    "filter": {
      "operator": "CONTAINS",
      "attribute": "$token.scope",
      "value": "cars.read"
    }
  }
}

Request to create the CAN_READ policy.

POST https://eu.api.indykite.com/configs/v1/authorization-policiesJson
{
  "project_id": "your_project_gid",
  "description": "Authorize CAN_READ on Car only when the access token's scope claim contains 'cars.read'.",
  "display_name": "policy - scope cars.read",
  "name": "policy-scope-cars-read",
  "policy": "{\"meta\":{\"policy_version\":\"2.0-kbac\"},\"subject\":{\"type\":\"Person\"},\"actions\":[\"CAN_READ\"],\"resource\":{\"type\":\"Car\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[:DRIVES]->(resource:Car)\",\"filter\":{\"operator\":\"CONTAINS\",\"attribute\":\"$token.scope\",\"value\":\"cars.read\"}}}",
  "status": "ACTIVE",
  "tags": []
}

Step 3

Policy: CAN_WRITE on Car requires the bearer token's scope claim to contain 'cars.write'.

policy_write.jsonJson
{
  "meta": {
    "policy_version": "2.0-kbac"
  },
  "subject": {
    "type": "Person"
  },
  "actions": [
    "CAN_WRITE"
  ],
  "resource": {
    "type": "Car"
  },
  "condition": {
    "cypher": "MATCH (subject:Person)-[:DRIVES]->(resource:Car)",
    "filter": {
      "operator": "CONTAINS",
      "attribute": "$token.scope",
      "value": "cars.write"
    }
  }
}

Request to create the CAN_WRITE policy.

POST https://eu.api.indykite.com/configs/v1/authorization-policiesJson
{
  "project_id": "your_project_gid",
  "description": "Authorize CAN_WRITE on Car only when the access token's scope claim contains 'cars.write'.",
  "display_name": "policy - scope cars.write",
  "name": "policy-scope-cars-write",
  "policy": "{\"meta\":{\"policy_version\":\"2.0-kbac\"},\"subject\":{\"type\":\"Person\"},\"actions\":[\"CAN_WRITE\"],\"resource\":{\"type\":\"Car\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[:DRIVES]->(resource:Car)\",\"filter\":{\"operator\":\"CONTAINS\",\"attribute\":\"$token.scope\",\"value\":\"cars.write\"}}}",
  "status": "ACTIVE",
  "tags": []
}

Step 4

Evaluation: Knight Rider asks if he can CAN_READ KITT. Send Authorization: Bearer <token> where the token's scope claim contains cars.read (plus X-IK-ClientKey for the AppAgent).

POST https://eu.api.indykite.com/access/v1/evaluationJson
{
  "subject": {
    "type": "Person",
    "id": "knightrider"
  },
  "resource": {
    "type": "Car",
    "id": "kitt"
  },
  "action": {
    "name": "CAN_READ"
  }
}

Response - authorized (DRIVES exists and the token scope contains cars.read).

Response 200Json
{
  "decision": true
}

Step 5

Same subject and resource, but action is CAN_WRITE and the same token only carries cars.read. The condition.filter scope check denies.

POST https://eu.api.indykite.com/access/v1/evaluationJson
{
  "subject": {
    "type": "Person",
    "id": "knightrider"
  },
  "resource": {
    "type": "Car",
    "id": "kitt"
  },
  "action": {
    "name": "CAN_WRITE"
  }
}

Response - denied. The DRIVES relationship is present but the scope check fails.

Response 200Json
{
  "decision": false
}

Same CAN_READ evaluation in Python (token passed in the Authorization header).

evaluate_scope_read.pyPython

import http.client
import json

conn = http.client.HTTPSConnection("eu.api.indykite.com")

payload = json.dumps({
  "subject": {"type": "Person", "id": "knightrider"},
  "resource": {"type": "Car", "id": "kitt"},
  "action": {"name": "CAN_READ"}
})

headers = {
  'Content-Type': "application/json",
  'X-IK-ClientKey': "",
  # User access token whose scope claim must contain 'cars.read'
  'Authorization': "Bearer <token>"
}

conn.request("POST", "/access/v1/evaluation", payload, headers)

res = conn.getresponse()
data = res.read()