Skip to content

Tutorial: Add Kessel to an existing application

Time: ~45 minutes

This tutorial walks you through integrating Kessel into a service you already have. By the end, your application will:

  1. Report resources to Kessel whenever it creates or deletes them
  2. Check permissions through a gRPC interceptor before serving requests

Unlike the Quick Start, which builds everything from scratch, this tutorial focuses on the integration points; the places where Kessel code lives alongside your existing application code.

Before writing any code, identify what your application manages and how it maps to Kessel’s model.

Ask yourself:

  • What are the “things” my application manages? These are your resource types.
  • How are they organized? Do resources belong to a parent container? In Kessel, the built-in workspace resource handles this grouping.
  • Who needs access, and what kind? What permissions does each resource type need?

For a task management service, the mapping looks like this:

Your applicationKessel concept
ProjectWorkspace (built-in)
TaskCustom resource type task
”Can view tasks”Permission task_view on workspace
”Can edit tasks”Permission task_edit on workspace
Team memberPrincipal with a role binding

Write a KSL schema for your resource type. If you followed the Quick Start, this process is familiar. Here is what a task management schema looks like:

version 0.1
namespace taskmanager
import rbac
@rbac.add_permission(name:'task_view')
@rbac.add_permission(name:'task_edit')
public type task {
relation workspace: [ExactlyOne rbac.workspace]
relation view: workspace.task_view
relation edit: workspace.task_edit
}

This defines two permissions (task_view, task_edit) that flow through the workspace hierarchy, and a task resource type that belongs to exactly one workspace.

For the full schema workflow (writing the base kessel.ksl and rbac.ksl files, compiling with ksl, creating resource type configuration, and loading into Kessel), see the Quick Start sections on configuring resources and installing Kessel. The steps below assume your schema is compiled and loaded.

  1. Install the SDK in your project. See the Quick Start prerequisites for language-specific install commands.

  2. Create a client.

    Set the KESSEL_ENDPOINT environment variable to your Kessel instance address (for example, localhost:9081).

    def create_kessel_client():
    stub, channel = (
    ClientBuilder(os.getenv("KESSEL_ENDPOINT"))
    .insecure()
    .build()
    )
    return stub, channel

Your service needs to tell Kessel about resources as they are created, updated, and deleted. The natural place to do this is right after your existing database operations.

The pattern is straightforward: after your service method successfully writes to the database, report the resource to Kessel. If the Kessel report fails, log the error but don’t fail the request. The resource exists in your database and Kessel will catch up on the next report.

The example below shows the create path. Deletes follow a similar pattern: after removing the resource from your database, call DeleteResource with a resource reference so Kessel removes it from the authorization graph. See the complete examples for the full create and delete implementation.

class TaskServicer:
"""gRPC servicer for the Task service."""
def __init__(self, kessel_stub, db, instance_id="taskmanager-1"):
self.kessel_stub = kessel_stub
self.db = db
self.instance_id = instance_id
def CreateTask(self, request, context):
# --- Your existing service logic ---
task = self.db.insert_task(request)
# --- Report to Kessel ---
common = struct_pb2.Struct()
common.update({"workspace_id": task.workspace_id})
reporter_data = struct_pb2.Struct()
reporter_data.update({"title": task.title, "status": task.status})
try:
self.kessel_stub.ReportResource(
report_resource_request_pb2.ReportResourceRequest(
type="task",
reporter_type="TASKMANAGER",
reporter_instance_id=self.instance_id,
representations=resource_representations_pb2.ResourceRepresentations(
metadata=representation_metadata_pb2.RepresentationMetadata(
local_resource_id=task.id,
api_href=f"{os.getenv('BASE_URL')}/api/tasks/{task.id}",
),
common=common,
reporter=reporter_data,
),
)
)
except grpc.RpcError as e:
# Log but don't fail. The resource exists in your database;
# Kessel will catch up on the next report or reconciliation.
logger.warning("kessel: failed to report task %s: %s - %s", task.id, e.code(), e.details())
return task

Key points:

  • Report after the database write, not before. The resource must exist in your system before Kessel knows about it.
  • Include the workspace_id in the common representation. This is what connects the resource to the workspace hierarchy and enables permission inheritance.
  • Handle errors gracefully. A Kessel outage should not prevent your service from creating resources. Log the failure and consider a reconciliation process to catch up.

The most impactful integration point is adding permission checks to your existing endpoints. Rather than scattering Check calls through every service method, wrap them in a gRPC server interceptor that runs before the handler executes.

The interceptor:

  1. Extracts the user identity from gRPC metadata and the resource ID from the request or metadata (the approach varies by language)
  2. Calls Kessel’s Check API
  3. Returns PERMISSION_DENIED if the user lacks the required permission
  4. Forwards the call to the handler if access is granted
class KesselAuthInterceptor(grpc.ServerInterceptor):
"""gRPC server interceptor that checks Kessel permissions before the handler runs."""
def __init__(self, kessel_stub, resource_type, reporter_type, relation):
self.kessel_stub = kessel_stub
self.resource_type = resource_type
self.reporter_type = reporter_type
self.relation = relation
def intercept_service(self, continuation, handler_call_details):
handler = continuation(handler_call_details)
if handler is None:
return None
metadata = dict(handler_call_details.invocation_metadata)
user_id = metadata.get("x-user-id")
resource_id = metadata.get("x-resource-id")
if not user_id:
return grpc.unary_unary_rpc_method_handler(
lambda req, ctx: ctx.abort(grpc.StatusCode.PERMISSION_DENIED, "missing user identity")
)
try:
response = self.kessel_stub.Check(
check_request_pb2.CheckRequest(
object=resource_reference_pb2.ResourceReference(
resource_type=self.resource_type,
resource_id=resource_id,
reporter=reporter_reference_pb2.ReporterReference(type=self.reporter_type),
),
relation=self.relation,
subject=subject_reference_pb2.SubjectReference(
resource=resource_reference_pb2.ResourceReference(
resource_type="principal",
resource_id=user_id,
reporter=reporter_reference_pb2.ReporterReference(type="rbac"),
),
),
)
)
except grpc.RpcError as e:
if e.code() == grpc.StatusCode.UNAVAILABLE:
return grpc.unary_unary_rpc_method_handler(
lambda req, ctx: ctx.abort(grpc.StatusCode.UNAVAILABLE, "authorization service unavailable")
)
return grpc.unary_unary_rpc_method_handler(
lambda req, ctx: ctx.abort(grpc.StatusCode.PERMISSION_DENIED, "forbidden")
)
if response.allowed != allowed_pb2.ALLOWED_TRUE:
return grpc.unary_unary_rpc_method_handler(
lambda req, ctx: ctx.abort(grpc.StatusCode.PERMISSION_DENIED, "forbidden")
)
return handler
# Wire it up when creating the gRPC server:
#
# view_interceptor = KesselAuthInterceptor(kessel_stub, "task", "TASKMANAGER", "view")
# edit_interceptor = KesselAuthInterceptor(kessel_stub, "task", "TASKMANAGER", "edit")
#
# server = grpc.server(
# futures.ThreadPoolExecutor(max_workers=10),
# interceptors=[view_interceptor],
# )

This pattern keeps authorization logic separate from business logic. Your service methods don’t need to know about Kessel; they only run if the interceptor allows the call through.

Kessel’s consistency model is tunable. By default, permission checks use minimize_latency, which may read slightly stale data. But you can achieve causal consistency (“read your writes”) when your use case requires it.

The two most important check methods are:

  • Check (default minimize_latency): fast, suitable for read-heavy paths like dashboards and list views. The authorization graph may lag the inventory database by 100-500ms under normal conditions, but this is invisible for most read operations.
  • CheckForUpdate: always fully consistent. The authorization backend evaluates against the latest committed state, including any recent changes to role bindings or resource relationships. Use this for updates, deletes, and any security-critical decision where acting on stale data could cause a conflict or incorrect access grant.

For writes, you can also use write_visibility: IMMEDIATE on ReportResource to wait for replication to complete before returning. This guarantees that a Check issued immediately after the report will reflect the change, giving you causal consistency between resource writes and reads.

For the full set of consistency modes, tokens, and strategies, see the Consistency model.

In this tutorial, you:

  • Mapped your resources to Kessel’s model, identifying resource types, workspaces, and permissions
  • Created a Kessel client using the SDK
  • Reported resources at your CRUD boundaries by adding Kessel reporting alongside your existing database operations
  • Checked permissions with a gRPC interceptor, gating requests on Kessel authorization without changing your service methods
  • Chose a consistency strategy using Check for reads and CheckForUpdate for writes and security-critical decisions

These are the integration seams you’ll use in any Kessel-enabled service. The patterns are the same whether your application manages tasks, hosts, clusters, or any other resource type.