Skip to content

Getting Started with RBAC

This tutorial walks you through setting up Kessel from scratch with a basic document management example. You’ll learn how to configure schemas, set up RBAC permissions, install client libraries, and implement both resource reporting and access control using Kessel’s APIs across multiple programming languages.

Let’s get some prerequisites out of the way. These are temporary solutions, so don’t worry about them too much.

  1. We need a schema compiler.

    Install the KSL compiler directly from GitHub:

    Terminal window
    go install github.com/project-kessel/ksl-schema-language/cmd/ksl@latest

    Ensure ksl is in your $PATH, or adjust the path in the scripts below.

  2. We need gRPCurl to perform some setup operations. gRPCurl is like curl, but for gRPC.

    Terminal window
    go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
  3. We need a clean working directory for some config files. If you don’t have one already, create one and change to it now.

    Terminal window
    mkdir -p kessel-getting-started
    cd kessel-getting-started
  4. Some built-in schemas need to be configured manually for now. We’ll talk more about schemas in the next section.

    Terminal window
    cat > kessel.ksl << 'EOF'
    version 0.1
    namespace kessel
    internal type lock {
    relation #version: [ExactlyOne lockversion]
    }
    internal type lockversion {}
    EOF
    Terminal window
    cat > rbac.ksl << 'EOF'
    version 0.1
    namespace rbac
    public type principal {}
    public type group {
    relation member: [Any principal or group.member]
    }
    public type role {
    }
    public type role_binding {
    relation subject: [Any principal or group.member]
    relation granted: [AtLeastOne role]
    }
    public type workspace {
    relation parent: [AtMostOne workspace]
    relation user_grant: [Any role_binding]
    }
    public extension add_permission(name) {
    type role {
    private relation `${name}`: [bool]
    }
    type role_binding {
    internal relation `${name}`: subject and granted.`${name}`
    }
    type workspace {
    internal relation `${name}`: user_grant.`${name}` or parent.`${name}`
    }
    }
    EOF

Now the interesting part. To protect and share your application’s resources, you first have to configure resource schema.

The following is an example of a drive like schema as described in Understanding Kessel. It will consistent of a simple document which is organized into Kessel’s Workspaces. It will have assignable permissions which leverage the common Kessel RBAC model.

  1. First we have to tell Kessel about the resource. We include its attributes and relationships, which are used for access control.

    Terminal window
    mkdir -p document/reporters/drive

    We configure a “document” resource, which has one reporter called “drive”.

    Terminal window
    cat > document/config.yaml << 'EOF'
    resource_type: document
    resource_reporters:
    - drive
    EOF

    Documents have a “common” representation (JSON data). Common representation is “common” regardless of the reporter. Here we configure the common representation schema. The workspace is common, because a document is always in a single workspace, regardless of how many reporters there are.

    Terminal window
    cat > document/common_representation.json << 'EOF'
    {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "properties": {
    "workspace_id": { "type": "string" }
    },
    "required": [
    "workspace_id"
    ]
    }
    EOF

    Reporters also have representations specific to their own state. Here we configure the “drive” reporter and its document representation schema.

    Terminal window
    cat > document/reporters/drive/config.yaml << 'EOF'
    resource_type: document
    reporter_name: drive
    namespace: drive
    EOF
    Terminal window
    cat > document/reporters/drive/document.json << 'EOF'
    {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "properties": {
    "document_id": { "type": "string" },
    "document_name": { "type": "string" },
    "document_type": {
    "type": "string",
    "enum": ["document"]
    },
    "created_at": {
    "type": "string",
    "format": "date-time"
    },
    "file_size": {
    "type": "integer",
    "minimum": 0
    },
    "owner_id": { "type": "string" }
    },
    "required": [
    "document_id",
    "document_name",
    "document_type"
    ]
    }
    EOF

    And finally we configure permissions.

    Terminal window
    cat > drive.ksl << 'EOF'
    version 0.1
    namespace drive
    import rbac
    // Extend RBAC model with relevant document permissions
    @rbac.add_permission(name:'view_document')
    @rbac.add_permission(name:'edit_document')
    @rbac.add_permission(name:'delete_document')
    public type document {
    relation workspace: [ExactlyOne rbac.workspace]
    // Define document permissions,
    // which are inherited from the workspace-level permissions
    // that we defined above.
    relation view: workspace.view_document
    relation edit: workspace.edit_document
    relation delete: workspace.delete_document
    }
    EOF
  2. Compile to SpiceDB schema.

    We installed the ksl schema compiler earlier. Use it now to compile the schemas:

    Terminal window
    ksl kessel.ksl rbac.ksl drive.ksl

    This will generate a schema.zed file in the current directory.

    Example generated schema.zed file
    schema.zed
    definition drive/document {
    permission edit = t_workspace->edit_document
    permission delete = t_workspace->delete_document
    permission view = t_workspace->view_document
    permission workspace = t_workspace
    relation t_workspace: rbac/workspace
    }
    definition kessel/lock {
    permission version = t_version
    relation t_version: kessel/lockversion
    }
    definition kessel/lockversion {}
    definition rbac/group {
    permission member = t_member
    relation t_member: rbac/principal | rbac/group#member
    }
    definition rbac/principal {}
    definition rbac/role {
    permission delete_document = t_delete_document
    relation t_delete_document: rbac/principal:*
    permission edit_document = t_edit_document
    relation t_edit_document: rbac/principal:*
    permission view_document = t_view_document
    relation t_view_document: rbac/principal:*
    }
    definition rbac/role_binding {
    permission delete_document = (subject & t_granted->delete_document)
    permission edit_document = (subject & t_granted->edit_document)
    permission granted = t_granted
    relation t_granted: rbac/role
    permission subject = t_subject
    relation t_subject: rbac/principal | rbac/group#member
    permission view_document = (subject & t_granted->view_document)
    }
    definition rbac/workspace {
    permission delete_document = t_user_grant->delete_document + t_parent->delete_document
    permission edit_document = t_user_grant->edit_document + t_parent->edit_document
    permission parent = t_parent
    relation t_parent: rbac/workspace
    permission user_grant = t_user_grant
    relation t_user_grant: rbac/role_binding
    permission view_document = t_user_grant->view_document + t_parent->view_document
    }
  1. Start Kessel Inventory. Inventory is the service that stores the resources and their relationships.

    Terminal window
    git clone git@github.com:project-kessel/inventory-api.git
    cd inventory-api
    cp -r ../document data/schema/resources/
    # preload the schema cache with the document resource
    go run main.go preload-schema
    make inventory-up-relations-ready
  2. Start Relations and SpiceDB. These form the underlying relationship storage and query engine based on relationship-based access control.

    Terminal window
    git clone git@github.com:project-kessel/relations-api.git
    cd relations-api
    cp ../schema.zed deploy/schema.zed
    make relations-api-up

Now Kessel is running. You configured the “built-in” RBAC schemas in prerequisites. Then you configured a document resource and extended RBAC resources like role and workspace with relevant permissions.

For a user to get access, three things need to come together:

  1. One or more permissions. These are aggregated into a role.
  2. A resource. These can be aggregated into a workspace (or other resources as defined by schema).
  3. A subject (e.g. a user). These can be aggregated into a group.

The union of these three things is a role binding. So for our next step, we will create a role to group together some permissions. Then, we’ll create a role binding by combining a role (the one we just created), a resource (some workspace), and a subject (some user).

This results in that user having the access described by the role to that workspace, and any documents with a “workspace” relation to it.

  1. Create a role

    A role defines a set of permissions. Edit the ROLE_NAME and PERMISSIONS variables to customize, then run the script.

    Terminal window
    #!/bin/bash
    # Create a role with specified permissions
    #
    # Usage: Edit the configuration below.
    #
    # The script can be copied directly into a terminal or saved as a file.
    # ============ CONFIGURATION ============
    # Modify these values for your use case
    ROLE_NAME="drive-admin-role"
    PERMISSIONS=("view_document" "edit_document" "delete_document")
    RELATIONS_PORT=9000
    # =======================================
    # Build the tuples array for all permissions
    TUPLES=""
    for i in "${!PERMISSIONS[@]}"; do
    PERM="${PERMISSIONS[$i]}"
    TUPLE="{\"resource\":{\"id\":\"${ROLE_NAME}\",\"type\":{\"name\":\"role\",\"namespace\":\"rbac\"}},\"relation\":\"${PERM}\",\"subject\":{\"subject\":{\"id\":\"*\",\"type\":{\"name\":\"principal\",\"namespace\":\"rbac\"}}}}"
    if [ $i -gt 0 ]; then
    TUPLES="${TUPLES},"
    fi
    TUPLES="${TUPLES}${TUPLE}"
    done
    MESSAGE="{\"tuples\":[${TUPLES}]}"
    echo "Creating role '${ROLE_NAME}' with permissions: ${PERMISSIONS[*]}"
    grpcurl -plaintext -d "${MESSAGE}" \
    "localhost:${RELATIONS_PORT}" \
    kessel.relations.v1beta1.KesselTupleService.CreateTuples
  2. Create a role binding

    A role binding grants a user access to a role for a specific resource (e.g., a workspace). Edit the ROLE_NAME, USER_ID, and RESOURCE_ID variables, then run the script. The binding ID is auto-generated from these inputs.

    Terminal window
    #!/bin/bash
    # Create a role binding that grants a user access to a role for a specific resource
    #
    # Usage: Edit the configuration below.
    #
    # The script can be copied directly into a terminal or saved as a file.
    # ============ CONFIGURATION ============
    # Modify these values for your use case
    ROLE_NAME="drive-admin-role"
    USER_ID="sarah"
    RESOURCE_ID="workspace-1"
    RELATIONS_PORT=9000
    # =======================================
    # Auto-generate a deterministic binding ID from the inputs
    BINDING_ID="${ROLE_NAME}--${USER_ID}--${RESOURCE_ID}"
    echo "Creating role binding '${BINDING_ID}'"
    echo " Role: ${ROLE_NAME}"
    echo " User: ${USER_ID}"
    echo " Resource: ${RESOURCE_ID}"
    # Create all three relationships in a single call:
    # 1. role_binding -> granted -> role
    # 2. role_binding -> subject -> user
    # 3. workspace -> user_grant -> role_binding
    MESSAGE="{\"tuples\":[
    {\"resource\":{\"id\":\"${BINDING_ID}\",\"type\":{\"name\":\"role_binding\",\"namespace\":\"rbac\"}},\"relation\":\"granted\",\"subject\":{\"subject\":{\"id\":\"${ROLE_NAME}\",\"type\":{\"name\":\"role\",\"namespace\":\"rbac\"}}}},
    {\"resource\":{\"id\":\"${BINDING_ID}\",\"type\":{\"name\":\"role_binding\",\"namespace\":\"rbac\"}},\"relation\":\"subject\",\"subject\":{\"subject\":{\"id\":\"${USER_ID}\",\"type\":{\"name\":\"principal\",\"namespace\":\"rbac\"}}}},
    {\"resource\":{\"id\":\"${RESOURCE_ID}\",\"type\":{\"name\":\"workspace\",\"namespace\":\"rbac\"}},\"relation\":\"user_grant\",\"subject\":{\"subject\":{\"id\":\"${BINDING_ID}\",\"type\":{\"name\":\"role_binding\",\"namespace\":\"rbac\"}}}}
    ]}"
    grpcurl -plaintext -d "${MESSAGE}" \
    "localhost:${RELATIONS_PORT}" \
    kessel.relations.v1beta1.KesselTupleService.CreateTuples

Kessel is configured and access is granted at the workspace level. Now onto your code and your resources.

We’ll start by setting up a client you can use to interact with Kessel. We have SDKs for many popular languages, or you can use the raw API with your favorite tool.

  1. Install dependencies.

    No SDK installation required. Just ensure curl is available:

    Terminal window
    curl --version
  2. Configure a client.

    Terminal window
    KESSEL_BASE_URL="http://localhost:9081"
    HEADERS=(
    -H "Content-Type: application/json"
    )

Before you can check access about a resource, Kessel needs to know about it. Kessel learns about resources by reporting them.

In this example, the document resource is reported which includes its relationship to a workspace. This inherits the access granted at at the workspace level, as defined by the document schema.

Terminal window
REPORT_MESSAGE='{"type": "document", "reporter_type": "drive", "reporter_instance_id": "drive-1","representations": {"metadata": {"local_resource_id": "doc-123","api_href": "https://drive.example.com/document/123","console_href": "https://www.console.com/drive/documents","reporter_version": "2.7.16"},"common": {"workspace_id": "workspace-1"},"reporter": {"document_id": "doc-123","document_name": "My Important Document","document_type": "document","created_at": "2025-08-31T10:30:00Z","file_size": 2048576,"owner_id": "user-1"}}}'
curl "${HEADERS[@]}" \
-X POST \
-d "$REPORT_MESSAGE" \
"$KESSEL_BASE_URL/api/inventory/v1beta2/resources"

Once Kessel knows about a resource (and its relationships), you can check access by virtue of checking if the user has an expected relationship to the resource, as defined by your schema.

Terminal window
# NOTE: You may need to wait for replication and caches to update.
CHECK_MESSAGE='{"object": {"resource_type": "document", "resource_id": "doc-123", "reporter": {"type": "drive"}}, "relation": "view", "subject": {"resource": {"resource_type": "principal", "resource_id": "sarah", "reporter": {"type": "rbac"}}}}'
curl "${HEADERS[@]}" \
-X POST \
-d "$CHECK_MESSAGE" \
"$KESSEL_BASE_URL/api/inventory/v1beta2/check"
#!/bin/bash
KESSEL_BASE_URL="http://localhost:9081"
HEADERS=(
-H "Content-Type: application/json"
)
REPORT_MESSAGE='{"type": "document", "reporter_type": "drive", "reporter_instance_id": "drive-1","representations": {"metadata": {"local_resource_id": "doc-123","api_href": "https://drive.example.com/document/123","console_href": "https://www.console.com/drive/documents","reporter_version": "2.7.16"},"common": {"workspace_id": "workspace-1"},"reporter": {"document_id": "doc-123","document_name": "My Important Document","document_type": "document","created_at": "2025-08-31T10:30:00Z","file_size": 2048576,"owner_id": "user-1"}}}'
curl "${HEADERS[@]}" \
-X POST \
-d "$REPORT_MESSAGE" \
"$KESSEL_BASE_URL/api/inventory/v1beta2/resources"
# NOTE: You may need to wait for replication and caches to update.
CHECK_MESSAGE='{"object": {"resource_type": "document", "resource_id": "doc-123", "reporter": {"type": "drive"}}, "relation": "view", "subject": {"resource": {"resource_type": "principal", "resource_id": "sarah", "reporter": {"type": "rbac"}}}}'
curl "${HEADERS[@]}" \
-X POST \
-d "$CHECK_MESSAGE" \
"$KESSEL_BASE_URL/api/inventory/v1beta2/check"

Try to experiment with the schema and relationships. Can you…

  • Create a new role?
  • Assign that role to a user?
  • Add a new permission to the document?

We’re open to contributions! Check us out on GitHub at project-kessel.

FeatureTodayRoadmap / What’s Next
Schema LanguageUse multiple files and languages for schema, with overlap and duplicated informationUnify all these into a single schema language—no duplication or repeated information
Configuration MethodEdit schema and config via filesConfigure everything at runtime via an API
RBAC ManagementUse temporary scripts or the pre-Kessel RBAC v1 APIUse an RBAC management API (v2) to encapsulate access grants and workspace management
Services to RunMust run the Relations API separately alongside Inventory and SpiceDBRelations API will be merged with Inventory—just run Inventory and SpiceDB