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.
Quick start
Section titled “Quick start”Prerequisites
Section titled “Prerequisites”Let’s get some prerequisites out of the way. These are temporary solutions, so don’t worry about them too much.
-
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@latestAlternatively, you can clone the repository and build locally:
Terminal window git clone https://github.com/project-kessel/ksl-schema-language.gitcd ksl-schema-languagemake buildEnsure
kslis in your$PATH, or adjust the path in the scripts below. -
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@latestTerminal window brew install grpcurlSee the grpcurl docs for additional installation methods including Snap, Docker, and prebuilt binaries.
-
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-startedcd kessel-getting-started -
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.1namespace kesselinternal type lock {relation #version: [ExactlyOne lockversion]}internal type lockversion {}EOFTerminal window cat > rbac.ksl << 'EOF'version 0.1namespace rbacpublic 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
Configure Resources
Section titled “Configure Resources”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.
-
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/driveWe configure a “document” resource, which has one reporter called “drive”.
Terminal window cat > document/config.yaml << 'EOF'resource_type: documentresource_reporters:- driveEOFDocuments 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"]}EOFReporters 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: documentreporter_name: drivenamespace: driveEOFTerminal 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"]}EOFAnd finally we configure permissions.
Terminal window cat > drive.ksl << 'EOF'version 0.1namespace driveimport 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_documentrelation edit: workspace.edit_documentrelation delete: workspace.delete_document}EOF -
Compile to SpiceDB schema.
We installed the
kslschema compiler earlier. Use it now to compile the schemas:Terminal window ksl kessel.ksl rbac.ksl drive.kslThis will generate a
schema.zedfile in the current directory.Example generated
schema.zedfileschema.zed definition drive/document {permission edit = t_workspace->edit_documentpermission delete = t_workspace->delete_documentpermission view = t_workspace->view_documentpermission workspace = t_workspacerelation t_workspace: rbac/workspace}definition kessel/lock {permission version = t_versionrelation t_version: kessel/lockversion}definition kessel/lockversion {}definition rbac/group {permission member = t_memberrelation t_member: rbac/principal | rbac/group#member}definition rbac/principal {}definition rbac/role {permission delete_document = t_delete_documentrelation t_delete_document: rbac/principal:*permission edit_document = t_edit_documentrelation t_edit_document: rbac/principal:*permission view_document = t_view_documentrelation 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_grantedrelation t_granted: rbac/rolepermission subject = t_subjectrelation t_subject: rbac/principal | rbac/group#memberpermission view_document = (subject & t_granted->view_document)}definition rbac/workspace {permission delete_document = t_user_grant->delete_document + t_parent->delete_documentpermission edit_document = t_user_grant->edit_document + t_parent->edit_documentpermission parent = t_parentrelation t_parent: rbac/workspacepermission user_grant = t_user_grantrelation t_user_grant: rbac/role_bindingpermission view_document = t_user_grant->view_document + t_parent->view_document}
Install and run Kessel
Section titled “Install and run Kessel”-
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.gitcd inventory-apicp -r ../document data/schema/resources/# preload the schema cache with the document resourcego run main.go preload-schemamake inventory-up-relations-ready -
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.gitcd relations-apicp ../schema.zed deploy/schema.zedmake relations-api-up
-
Start Kessel Inventory
Terminal window bonfire deploy kessel -C kessel-inventory -
Load Schema
Terminal window # create configmap from the schema.zed file we created earlieroc create configmap spicedb-schema --dry-run=client --from-file=schema.zed -o yaml > spicedb-schema-configmap.yaml# apply the configmapoc apply -f spicedb-schema-configmap.yaml -
Fetch the API URL and Credentials
Terminal window bonfire namespace describe -
Enable port forwarding to Kessel Inventory
Terminal window oc port-forward svc/kessel-inventory-api <your local port>:9000 -n <Name of current project from step 3>oc port-forward svc/kessel-relations-api <your local port>:9000 -n <Name of current project from step 3>
Set up access with RBAC
Section titled “Set up access with RBAC”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:
- One or more permissions. These are aggregated into a role.
- A resource. These can be aggregated into a workspace (or other resources as defined by schema).
- 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.
-
Create a role
A role defines a set of permissions. Edit the
ROLE_NAMEandPERMISSIONSvariables 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 caseROLE_NAME="drive-admin-role"PERMISSIONS=("view_document" "edit_document" "delete_document")RELATIONS_PORT=9000# =======================================# Build the tuples array for all permissionsTUPLES=""for i in "${!PERMISSIONS[@]}"; doPERM="${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 ]; thenTUPLES="${TUPLES},"fiTUPLES="${TUPLES}${TUPLE}"doneMESSAGE="{\"tuples\":[${TUPLES}]}"echo "Creating role '${ROLE_NAME}' with permissions: ${PERMISSIONS[*]}"grpcurl -plaintext -d "${MESSAGE}" \"localhost:${RELATIONS_PORT}" \kessel.relations.v1beta1.KesselTupleService.CreateTuples -
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, andRESOURCE_IDvariables, 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 caseROLE_NAME="drive-admin-role"USER_ID="sarah"RESOURCE_ID="workspace-1"RELATIONS_PORT=9000# =======================================# Auto-generate a deterministic binding ID from the inputsBINDING_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_bindingMESSAGE="{\"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
Setting up your environment
Section titled “Setting up your environment”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.
-
Install dependencies.
No SDK installation required. Just ensure
curlis available:Terminal window curl --versionIf you followed along with Prerequisites, you’re good to go.
Terminal window pip install kessel-sdkTerminal window go get github.com/project-kessel/kessel-sdk-goTerminal window npm install @project-kessel/kessel-sdkTerminal window gem install kessel-sdk<dependency><groupId>io.grpc</groupId><artifactId>grpc-netty-shaded</artifactId></dependency><dependency><groupId>org.project-kessel</groupId><artifactId>kessel-sdk</artifactId><version>1.0.0</version></dependency>dependencies {implementation 'io.grpc:grpc-netty-shaded'implementation "org.project-kessel:kessel-sdk:1.0.0"} -
Configure a client.
Terminal window KESSEL_BASE_URL="http://localhost:9081"HEADERS=(-H "Content-Type: application/json")Terminal window # Set your Kessel gRPC endpointKESSEL_GRPC_ENDPOINT="localhost:9081"# For insecure local development:GRPC_OPTS="-plaintext"# For insecure local development:stub, channel = ClientBuilder(KESSEL_ENDPOINT).insecure().build()inventoryClient, conn, err := v1beta2.NewClientBuilder(KESSEL_ENDPOINT).Insecure().Build()if err != nil {log.Fatal("Failed to create gRPC client:", err)}defer func() {if closeErr := conn.Close(); closeErr != nil {log.Printf("Failed to close gRPC client: %v", closeErr)}}()// For insecure local development:const client = new ClientBuilder(process.env.KESSEL_ENDPOINT).insecure().buildAsync();# For insecure local development:client = KesselInventoryService::ClientBuilder.new(ENV.fetch('KESSEL_ENDPOINT', 'localhost:9081')).insecure.buildString kesselEndpoint = System.getenv().getOrDefault("KESSEL_ENDPOINT", "localhost:9081");Pair<KesselInventoryServiceBlockingStub, ManagedChannel> clientAndChannel =new ClientBuilder(kesselEndpoint).insecure().build();KesselInventoryServiceBlockingStub client = clientAndChannel.getLeft();
Reporting resources
Section titled “Reporting resources”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.
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"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"}}}'grpcurl $GRPC_OPTS \ -d "$REPORT_MESSAGE" \ "$KESSEL_GRPC_ENDPOINT" \ kessel.inventory.v1beta2.KesselInventoryService.ReportResourcecommon_struct = struct_pb2.Struct()common_struct.update({"workspace_id": "workspace-1"})
reporter_struct = struct_pb2.Struct()reporter_struct.update({ "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",})
metadata = representation_metadata_pb2.RepresentationMetadata( 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",)
representations = resource_representations_pb2.ResourceRepresentations( metadata=metadata, common=common_struct, reporter=reporter_struct,)
report_req = report_resource_request_pb2.ReportResourceRequest( type="document", reporter_type="drive", reporter_instance_id="drive-1", representations=representations,)stub.ReportResource(report_req)reportResourceRequest := &v1beta2.ReportResourceRequest{ Type: "document", ReporterType: "drive", ReporterInstanceId: "drive-1", Representations: &v1beta2.ResourceRepresentations{ Metadata: &v1beta2.RepresentationMetadata{ LocalResourceId: "doc-123", ApiHref: "https://drive.example.com/document/123", ConsoleHref: addr("https://www.console.com/drive/documents"), ReporterVersion: addr("2.7.16"), }, Common: &structpb.Struct{ Fields: map[string]*structpb.Value{ "workspace_id": structpb.NewStringValue("workspace-1"), }, }, Reporter: &structpb.Struct{ Fields: map[string]*structpb.Value{ "document_id": structpb.NewStringValue("doc-123"), "document_name": structpb.NewStringValue("My Important Document"), "document_type": structpb.NewStringValue("document"), "created_at": structpb.NewStringValue("2025-08-31T10:30:00Z"), "file_size": structpb.NewNumberValue(2048576), "owner_id": structpb.NewStringValue("user-1"), }, }, },}
_, err = inventoryClient.ReportResource(ctx, reportResourceRequest)if err != nil { log.Fatalf("ReportResource failed: %v", err)}const common = { workspace_id: "workspace-1" };const 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",};const metadata: RepresentationMetadata = { localResourceId: "doc-123", apiHref: "https://drive.example.com/document/123", consoleHref: "https://www.console.com/drive/documents", reporterVersion: "2.7.16",};const representations: ResourceRepresentations = { metadata, common, reporter,};const reportResourceRequest: ReportResourceRequest = { type: "document", reporterType: "drive", reporterInstanceId: "drive-1", representations,};await client.reportResource(reportResourceRequest);common = Google::Protobuf::Struct.decode_json({ 'workspace_id' => 'workspace-1' }.to_json)reporter = Google::Protobuf::Struct.decode_json({ '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'}.to_json)metadata = RepresentationMetadata.new( 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')representations = ResourceRepresentations.new( metadata: metadata, common: common, reporter: reporter)client.report_resource( ReportResourceRequest.new( type: 'document', reporter_type: 'drive', reporter_instance_id: 'drive-1', representations: representations ))ReportResourceRequest reportResourceRequest = ReportResourceRequest .newBuilder() .setType("document") .setReporterType("drive") .setReporterInstanceId("drive-1") .setRepresentations( ResourceRepresentations .newBuilder() .setMetadata( RepresentationMetadata .newBuilder() .setLocalResourceId("doc-123") .setApiHref("https://drive.example.com/document/123") .setConsoleHref("https://www.console.com/drive/documents") .setReporterVersion("2.7.16") .build() ) .setCommon( Struct .newBuilder() .putFields("workspace_id", Value.newBuilder().setStringValue("workspace-1").build()) .build() ) .setReporter( Struct .newBuilder() .putFields("document_id", Value.newBuilder().setStringValue("doc-123").build()) .putFields("document_name", Value.newBuilder().setStringValue("My Important Document").build()) .putFields("document_type", Value.newBuilder().setStringValue("document").build()) .putFields("created_at", Value.newBuilder().setStringValue("2025-08-31T10:30:00Z").build()) .putFields("file_size", Value.newBuilder().setNumberValue(2048576).build()) .putFields("owner_id", Value.newBuilder().setStringValue("user-1").build()) .build() ) .build() ) .build();
client.reportResource(reportResourceRequest);Checking permissions
Section titled “Checking permissions”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.
# 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"# 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"}}}}'grpcurl $GRPC_OPTS \ -d "$CHECK_MESSAGE" \ "$KESSEL_GRPC_ENDPOINT" \ kessel.inventory.v1beta2.KesselInventoryService.Check# NOTE: You may need to wait for replication and caches to update.subject = subject_reference_pb2.SubjectReference( resource=resource_reference_pb2.ResourceReference( reporter=reporter_reference_pb2.ReporterReference(type="rbac"), resource_id="sarah", resource_type="principal", ))
resource_ref = resource_reference_pb2.ResourceReference( resource_id="doc-123", resource_type="document", reporter=reporter_reference_pb2.ReporterReference(type="drive"),)
check_request = check_request_pb2.CheckRequest( subject=subject, relation="view", object=resource_ref,)
try: check_response = stub.Check(check_request) print("Check response received successfully") print(check_response)except grpc.RpcError as e: print("gRPC error occurred during Check:") print(f"Code: {e.code()}") print(f"Details: {e.details()}")// NOTE: You may need to wait for replication and caches to update.checkRequest := &v1beta2.CheckRequest{ Object: &v1beta2.ResourceReference{ ResourceType: "document", ResourceId: "doc-123", Reporter: &v1beta2.ReporterReference{Type: "drive"}, }, Relation: "view", Subject: &v1beta2.SubjectReference{ Resource: &v1beta2.ResourceReference{ ResourceType: "principal", ResourceId: "sarah", Reporter: &v1beta2.ReporterReference{Type: "rbac"}, }, },}
fmt.Println("Making check request:")response, err := inventoryClient.Check(ctx, checkRequest)if err != nil { if st, ok := status.FromError(err); ok { switch st.Code() { case codes.Unavailable: log.Fatal("Service unavailable: ", err) case codes.PermissionDenied: log.Fatal("Permission denied: ", err) default: log.Fatal("gRPC connection error: ", err) } } else { log.Fatal("Unknown error: ", err) }}fmt.Printf("Check response: %+v\n", response)// NOTE: You may need to wait for replication and caches to update.const subjectReference: SubjectReference = { resource: { reporter: { type: "rbac" }, resourceId: "sarah", resourceType: "principal", },};const resource: ResourceReference = { reporter: { type: "drive" }, resourceId: "doc-123", resourceType: "document",};const check_request: CheckRequest = { object: resource, relation: "view", subject: subjectReference,};const response = await client.check(check_request);console.log("Check response received successfully:");console.log(response);# NOTE: You may need to wait for replication and caches to update.subject_reference = SubjectReference.new( resource: ResourceReference.new( reporter: ReporterReference.new(type: 'rbac'), resource_id: 'sarah', resource_type: 'principal' ))resource = ResourceReference.new( reporter: ReporterReference.new(type: 'drive'), resource_id: 'doc-123', resource_type: 'document')
begin response = client.check( CheckRequest.new( object: resource, relation: 'view', subject: subject_reference ) ) p 'check response received successfully:' p responserescue => e p 'gRPC error occurred during check:' p "Exception: #{e}"end// NOTE: You may need to wait for replication and caches to update.CheckRequest checkRequest = CheckRequest .newBuilder() .setObject( ResourceReference .newBuilder() .setReporter(ReporterReference.newBuilder().setType("drive").build()) .setResourceId("doc-123") .setResourceType("document") .build() ) .setRelation("view") .setSubject( SubjectReference .newBuilder() .setResource( ResourceReference .newBuilder() .setReporter(ReporterReference.newBuilder().setType("rbac").build()) .setResourceId("sarah") .setResourceType("principal") .build() ) .build() ) .build();
CheckResponse response = client.check(checkRequest);System.out.println("Check response received successfully:");System.out.println(response);Complete example
Section titled “Complete example”#!/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"#!/bin/bash
# Set your Kessel gRPC endpointKESSEL_GRPC_ENDPOINT="localhost:9081"
# For insecure local development:GRPC_OPTS="-plaintext"
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"}}}'grpcurl $GRPC_OPTS \ -d "$REPORT_MESSAGE" \ "$KESSEL_GRPC_ENDPOINT" \ kessel.inventory.v1beta2.KesselInventoryService.ReportResource
# 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"}}}}'grpcurl $GRPC_OPTS \ -d "$CHECK_MESSAGE" \ "$KESSEL_GRPC_ENDPOINT" \ kessel.inventory.v1beta2.KesselInventoryService.Checkimport grpcfrom google.protobuf import struct_pb2from kessel.inventory.v1beta2 import ( ClientBuilder, subject_reference_pb2, resource_reference_pb2, reporter_reference_pb2, check_request_pb2, report_resource_request_pb2, resource_representations_pb2, representation_metadata_pb2,)
KESSEL_ENDPOINT = "localhost:9081"
def main(): # For insecure local development: stub, channel = ClientBuilder(KESSEL_ENDPOINT).insecure().build()
with channel: common_struct = struct_pb2.Struct() common_struct.update({"workspace_id": "workspace-1"})
reporter_struct = struct_pb2.Struct() reporter_struct.update({ "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", })
metadata = representation_metadata_pb2.RepresentationMetadata( 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", )
representations = resource_representations_pb2.ResourceRepresentations( metadata=metadata, common=common_struct, reporter=reporter_struct, )
report_req = report_resource_request_pb2.ReportResourceRequest( type="document", reporter_type="drive", reporter_instance_id="drive-1", representations=representations, ) stub.ReportResource(report_req)
# NOTE: You may need to wait for replication and caches to update. subject = subject_reference_pb2.SubjectReference( resource=resource_reference_pb2.ResourceReference( reporter=reporter_reference_pb2.ReporterReference(type="rbac"), resource_id="sarah", resource_type="principal", ) )
resource_ref = resource_reference_pb2.ResourceReference( resource_id="doc-123", resource_type="document", reporter=reporter_reference_pb2.ReporterReference(type="drive"), )
check_request = check_request_pb2.CheckRequest( subject=subject, relation="view", object=resource_ref, )
try: check_response = stub.Check(check_request) print("Check response received successfully") print(check_response) except grpc.RpcError as e: print("gRPC error occurred during Check:") print(f"Code: {e.code()}") print(f"Details: {e.details()}")
if __name__ == "__main__": main()package main
import ( "context" "fmt" "log"
_ "github.com/joho/godotenv/autoload"
"google.golang.org/protobuf/types/known/structpb"
"github.com/project-kessel/kessel-sdk-go/kessel/inventory/v1beta2" "google.golang.org/grpc/codes" "google.golang.org/grpc/status")
const KESSEL_ENDPOINT = "localhost:9081"
func main() { ctx := context.Background()
inventoryClient, conn, err := v1beta2.NewClientBuilder(KESSEL_ENDPOINT). Insecure(). Build() if err != nil { log.Fatal("Failed to create gRPC client:", err) } defer func() { if closeErr := conn.Close(); closeErr != nil { log.Printf("Failed to close gRPC client: %v", closeErr) } }()
reportResourceRequest := &v1beta2.ReportResourceRequest{ Type: "document", ReporterType: "drive", ReporterInstanceId: "drive-1", Representations: &v1beta2.ResourceRepresentations{ Metadata: &v1beta2.RepresentationMetadata{ LocalResourceId: "doc-123", ApiHref: "https://drive.example.com/document/123", ConsoleHref: addr("https://www.console.com/drive/documents"), ReporterVersion: addr("2.7.16"), }, Common: &structpb.Struct{ Fields: map[string]*structpb.Value{ "workspace_id": structpb.NewStringValue("workspace-1"), }, }, Reporter: &structpb.Struct{ Fields: map[string]*structpb.Value{ "document_id": structpb.NewStringValue("doc-123"), "document_name": structpb.NewStringValue("My Important Document"), "document_type": structpb.NewStringValue("document"), "created_at": structpb.NewStringValue("2025-08-31T10:30:00Z"), "file_size": structpb.NewNumberValue(2048576), "owner_id": structpb.NewStringValue("user-1"), }, }, }, }
_, err = inventoryClient.ReportResource(ctx, reportResourceRequest) if err != nil { log.Fatalf("ReportResource failed: %v", err) }
// NOTE: You may need to wait for replication and caches to update. checkRequest := &v1beta2.CheckRequest{ Object: &v1beta2.ResourceReference{ ResourceType: "document", ResourceId: "doc-123", Reporter: &v1beta2.ReporterReference{Type: "drive"}, }, Relation: "view", Subject: &v1beta2.SubjectReference{ Resource: &v1beta2.ResourceReference{ ResourceType: "principal", ResourceId: "sarah", Reporter: &v1beta2.ReporterReference{Type: "rbac"}, }, }, }
fmt.Println("Making check request:") response, err := inventoryClient.Check(ctx, checkRequest) if err != nil { if st, ok := status.FromError(err); ok { switch st.Code() { case codes.Unavailable: log.Fatal("Service unavailable: ", err) case codes.PermissionDenied: log.Fatal("Permission denied: ", err) default: log.Fatal("gRPC connection error: ", err) } } else { log.Fatal("Unknown error: ", err) } } fmt.Printf("Check response: %+v\n", response)}
func addr[T any](t T) *T { return &t }// example.ts - Runnable JavaScript/TypeScript SDK exampleimport { ResourceReference } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2/resource_reference";import { SubjectReference } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2/subject_reference";import { CheckRequest } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2/check_request";import { ReportResourceRequest } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2/report_resource_request";import { ResourceRepresentations } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2/resource_representations";import { RepresentationMetadata } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2/representation_metadata";import { ClientBuilder } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2";import "dotenv/config";
async function run() { // For insecure local development: const client = new ClientBuilder(process.env.KESSEL_ENDPOINT).insecure().buildAsync();
try { const common = { workspace_id: "workspace-1" }; const 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", }; const metadata: RepresentationMetadata = { localResourceId: "doc-123", apiHref: "https://drive.example.com/document/123", consoleHref: "https://www.console.com/drive/documents", reporterVersion: "2.7.16", }; const representations: ResourceRepresentations = { metadata, common, reporter, }; const reportResourceRequest: ReportResourceRequest = { type: "document", reporterType: "drive", reporterInstanceId: "drive-1", representations, }; await client.reportResource(reportResourceRequest);
// NOTE: You may need to wait for replication and caches to update. const subjectReference: SubjectReference = { resource: { reporter: { type: "rbac" }, resourceId: "sarah", resourceType: "principal", }, }; const resource: ResourceReference = { reporter: { type: "drive" }, resourceId: "doc-123", resourceType: "document", }; const check_request: CheckRequest = { object: resource, relation: "view", subject: subjectReference, }; const response = await client.check(check_request); console.log("Check response received successfully:"); console.log(response); } catch (error) { console.log("Error during report or check:"); console.log("Exception:", error); }}
run();#!/usr/bin/env ruby# frozen_string_literal: true# example.rb - Runnable Ruby SDK example
require 'dotenv/load'require 'json'require 'kessel-sdk'
include Kessel::Inventory::V1beta2include Kessel::GRPCinclude Kessel::Auth
# For insecure local development:client = KesselInventoryService::ClientBuilder.new(ENV.fetch('KESSEL_ENDPOINT', 'localhost:9081')) .insecure .build
common = Google::Protobuf::Struct.decode_json({ 'workspace_id' => 'workspace-1' }.to_json)reporter = Google::Protobuf::Struct.decode_json({ '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'}.to_json)metadata = RepresentationMetadata.new( 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')representations = ResourceRepresentations.new( metadata: metadata, common: common, reporter: reporter)client.report_resource( ReportResourceRequest.new( type: 'document', reporter_type: 'drive', reporter_instance_id: 'drive-1', representations: representations ))
# NOTE: You may need to wait for replication and caches to update.subject_reference = SubjectReference.new( resource: ResourceReference.new( reporter: ReporterReference.new(type: 'rbac'), resource_id: 'sarah', resource_type: 'principal' ))resource = ResourceReference.new( reporter: ReporterReference.new(type: 'drive'), resource_id: 'doc-123', resource_type: 'document')
begin response = client.check( CheckRequest.new( object: resource, relation: 'view', subject: subject_reference ) ) p 'check response received successfully:' p responserescue => e p 'gRPC error occurred during check:' p "Exception: #{e}"endimport com.google.protobuf.Struct;import com.google.protobuf.Value;import com.nimbusds.jose.util.Pair;import io.grpc.ManagedChannel;import io.grpc.StatusRuntimeException;import static org.project_kessel.api.inventory.v1beta2.KesselInventoryServiceGrpc.KesselInventoryServiceBlockingStub;import org.project_kessel.api.inventory.v1beta2.*;
public class Example { public static void main(String[] args) { String kesselEndpoint = System.getenv().getOrDefault("KESSEL_ENDPOINT", "localhost:9081");
Pair<KesselInventoryServiceBlockingStub, ManagedChannel> clientAndChannel = new ClientBuilder(kesselEndpoint) .insecure() .build(); KesselInventoryServiceBlockingStub client = clientAndChannel.getLeft();
try { ReportResourceRequest reportResourceRequest = ReportResourceRequest .newBuilder() .setType("document") .setReporterType("drive") .setReporterInstanceId("drive-1") .setRepresentations( ResourceRepresentations .newBuilder() .setMetadata( RepresentationMetadata .newBuilder() .setLocalResourceId("doc-123") .setApiHref("https://drive.example.com/document/123") .setConsoleHref("https://www.console.com/drive/documents") .setReporterVersion("2.7.16") .build() ) .setCommon( Struct .newBuilder() .putFields("workspace_id", Value.newBuilder().setStringValue("workspace-1").build()) .build() ) .setReporter( Struct .newBuilder() .putFields("document_id", Value.newBuilder().setStringValue("doc-123").build()) .putFields("document_name", Value.newBuilder().setStringValue("My Important Document").build()) .putFields("document_type", Value.newBuilder().setStringValue("document").build()) .putFields("created_at", Value.newBuilder().setStringValue("2025-08-31T10:30:00Z").build()) .putFields("file_size", Value.newBuilder().setNumberValue(2048576).build()) .putFields("owner_id", Value.newBuilder().setStringValue("user-1").build()) .build() ) .build() ) .build();
client.reportResource(reportResourceRequest);
// NOTE: You may need to wait for replication and caches to update. CheckRequest checkRequest = CheckRequest .newBuilder() .setObject( ResourceReference .newBuilder() .setReporter(ReporterReference.newBuilder().setType("drive").build()) .setResourceId("doc-123") .setResourceType("document") .build() ) .setRelation("view") .setSubject( SubjectReference .newBuilder() .setResource( ResourceReference .newBuilder() .setReporter(ReporterReference.newBuilder().setType("rbac").build()) .setResourceId("sarah") .setResourceType("principal") .build() ) .build() ) .build();
CheckResponse response = client.check(checkRequest); System.out.println("Check response received successfully:"); System.out.println(response); } catch (StatusRuntimeException statusException) { System.out.println("gRPC error occurred:"); statusException.printStackTrace(); } finally { clientAndChannel.getRight().shutdown(); } }}Next steps and roadmap
Section titled “Next steps and roadmap”Exercises
Section titled “Exercises”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?
How to contribute
Section titled “How to contribute”We’re open to contributions! Check us out on GitHub at project-kessel.
Where are we going?
Section titled “Where are we going?”| Feature | Today | Roadmap / What’s Next |
|---|---|---|
| Schema Language | Use multiple files and languages for schema, with overlap and duplicated information | Unify all these into a single schema language—no duplication or repeated information |
| Configuration Method | Edit schema and config via files | Configure everything at runtime via an API |
| RBAC Management | Use temporary scripts or the pre-Kessel RBAC v1 API | Use an RBAC management API (v2) to encapsulate access grants and workspace management |
| Services to Run | Must run the Relations API separately alongside Inventory and SpiceDB | Relations API will be merged with Inventory—just run Inventory and SpiceDB |