Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const baseStarlightConfig = {
items: [
"start-here/getting-started",
"start-here/understanding-kessel",
"start-here/add-kessel-to-app",
"start-here/roadmap",
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@ run the [integration test](#run-the-integration-test) or interact directly with
</Steps>

<Aside type="note">
The first run downloads several container images and may take a few minutes. Subsequent starts reuse cached images and are much faster.
The Kafka infrastructure typically takes about 60 seconds to become fully ready.
Each run ensures the latest images are pulled down to avoid issues with stale versions of each service. The Kafka infrastructure typically takes about 60 seconds to become fully ready.
</Aside>

To check that all containers are running and healthy (Docker users: substitute `docker` for `podman`):
Expand Down
169 changes: 169 additions & 0 deletions src/content/docs/start-here/add-kessel-to-app.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
---
title: "Tutorial: Add Kessel to an existing application"
description: Integrate Kessel authorization into your existing service by mapping resources, setting up authenticated clients, reporting at CRUD boundaries, and checking permissions with gRPC interceptors.
---

import { Aside, CardGrid, LinkCard, Steps } from '@astrojs/starlight/components';
import CodeExamples from 'src/components/CodeExamples.astro';

export const integrationExamples = import.meta.glob('/src/examples/integrate/example.*', { query: '?raw', eager: true, import: 'default' });

**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](../getting-started/), which builds everything from scratch, this tutorial focuses on the **integration points**; the places where Kessel code lives alongside your existing application code.

<Aside type="tip" title="Complete the Quick Start first">
This tutorial assumes you have completed the [Quick Start](../getting-started/) and have a running Kessel instance with a schema loaded. You should understand resources, schema, roles, and permission checks before continuing.
</Aside>

## Map your resources

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 application | Kessel concept |
|---|---|
| Project | Workspace (built-in) |
| Task | Custom resource type `task` |
| "Can view tasks" | Permission `task_view` on workspace |
| "Can edit tasks" | Permission `task_edit` on workspace |
| Team member | Principal with a role binding |

<Aside type="note">
Not every application concept maps directly to a Kessel concept. The goal is to identify the resources that need access control and find the right level of granularity. See [Design permissions](/docs/building-with-kessel/how-to/design-permissions/) for guidance on common patterns.
</Aside>

## Design your schema

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:

```ksl
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](../getting-started/#configure-resources) and [installing Kessel](../getting-started/#install-and-run-kessel). The steps below assume your schema is compiled and loaded.

<Aside type="tip" title="Need the complete files?">
The KSL schema, resource type configuration, and full working code examples for this tutorial are available in the [examples/integrate](https://github.com/project-kessel/docs/tree/main/src/examples/integrate) directory. Use them as a reference if you get stuck.
</Aside>

## Add the SDK and create an authenticated client

The Quick Start uses `insecure()` mode for local development. For a real integration, configure the SDK with OAuth2 credentials and TLS.

<Steps>
1. **Install the SDK** in your project. See the [Quick Start prerequisites](../getting-started/#set-up-your-environment) for language-specific install commands.

2. **Create an authenticated client.**

The examples below use environment variables for credentials. In production, your service account credentials come from your platform's secret management (for example, Vault or app-interface secrets).

<CodeExamples files={integrationExamples} regions="client-setup" />

For a detailed walkthrough of TLS certificate loading and OAuth2 configuration, see [Enable TLS](/docs/building-with-kessel/how-to/enable-tls/).
</Steps>

<Aside type="tip" title="Local development">
When developing locally against a Kessel instance started with `make kessel-up`, use the `insecure()` builder from the [Quick Start](../getting-started/#set-up-your-environment) instead. Switch to authenticated clients when deploying to shared environments.
</Aside>

## Report resources at your CRUD boundaries

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.

<CodeExamples files={integrationExamples} regions="report-on-create" />

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.

<Aside type="note">
For high-throughput services, reporting inline may not be practical. See [Report resources](/docs/building-with-kessel/how-to/report-resources/) for async alternatives using the outbox pattern or Kafka consumers.
</Aside>

## Check permissions before serving requests

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 and resource ID from gRPC metadata
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

<CodeExamples files={integrationExamples} regions="check-middleware" />

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.

<Aside type="caution" title="Adapt to your framework">
The examples above show gRPC server interceptors for Go, Python, and TypeScript. The same pattern applies to any gRPC framework: extract user and resource identifiers from metadata, call `Check`, and gate the request on the result.
</Aside>

## Choose your consistency strategy

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](/docs/building-with-kessel/concepts/consistency/).

## What you learned

In this tutorial, you:

- **Mapped your resources** to Kessel's model, identifying resource types, workspaces, and permissions
- **Created an authenticated client** with OAuth2 and TLS for production use
- **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.

## Next steps

<CardGrid>
<LinkCard title="Report resources" href="/docs/building-with-kessel/how-to/report-resources/" description="Async reporting, outbox pattern, and Kafka consumers for high-throughput services." />
<LinkCard title="Protect an endpoint" href="/docs/building-with-kessel/how-to/protect-endpoint/" description="Expanded patterns for endpoint protection including Check vs CheckForUpdate." />
<LinkCard title="Design permissions" href="/docs/building-with-kessel/how-to/design-permissions/" description="Authorization patterns for hierarchical resources, shared access, and org-wide settings." />
<LinkCard title="Consistency model" href="/docs/building-with-kessel/concepts/consistency/" description="Tunable consistency modes, write visibility, and strategies for handling replication." />
</CardGrid>

<Aside type="tip" title="Migrating from RBAC v1?">
If your application currently uses the legacy Insights RBAC system, see [Migrate from RBAC v1 to RBAC v2](/docs/building-with-kessel/how-to/migrate-from-rbac-v1-to-v2/) for specific guidance on converting permission definitions and adapting authorization checks.
</Aside>
15 changes: 15 additions & 0 deletions src/content/docs/start-here/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import createRoleBindingScript from 'src/examples/scripts/create-role-binding.sh

export const createFile = (path, content) => `cat > ${path} << 'EOF'\n${content.trim()}\nEOF`;

**Time: ~30 minutes**

This tutorial walks you through setting up Kessel from scratch. By the end, you will have:

1. **Run Kessel** locally with Docker Compose
Expand Down Expand Up @@ -344,6 +346,19 @@ Kessel provides several ways to handle this, including fully consistent checks (

<CodeExamples files={gettingStartedExamples} />

## What you learned

In this tutorial, you:

- **Defined a resource type** using KSL schema files and JSON Schema for resource attributes
- **Compiled the schema** to SpiceDB format using the `ksl` compiler
- **Ran the full Kessel stack** locally with Docker Compose
- **Set up RBAC** by creating a role with permissions and binding it to a user on a workspace
- **Reported a resource** to Kessel's inventory, including its relationship to a workspace
- **Checked permissions** to verify that access control works through the relationship graph

These are the same building blocks you'll use when integrating Kessel into a real application. The [how-to guides](/docs/building-with-kessel/how-to/report-resources/) go deeper into each step.

## Try it yourself

Experiment with the schema and relationships. Can you:
Expand Down
167 changes: 167 additions & 0 deletions src/examples/integrate/example.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package main

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"os"

"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb"

"github.com/project-kessel/kessel-sdk-go/kessel/auth"
kesselgrpc "github.com/project-kessel/kessel-sdk-go/kessel/grpc"
"github.com/project-kessel/kessel-sdk-go/kessel/inventory/v1beta2"
)

//#region client-setup
func newKesselClient() (v1beta2.KesselInventoryServiceClient, func(), error) {
oauthCreds := auth.NewOAuth2ClientCredentials(
os.Getenv("KESSEL_CLIENT_ID"),
os.Getenv("KESSEL_CLIENT_SECRET"),
os.Getenv("KESSEL_TOKEN_ENDPOINT"),
)

caCert, err := os.ReadFile(os.Getenv("KESSEL_CA_CERT_PATH"))
if err != nil {
return nil, nil, fmt.Errorf("read CA cert: %w", err)
}
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(caCert)
tlsCreds := credentials.NewTLS(&tls.Config{RootCAs: certPool})

client, conn, err := v1beta2.NewClientBuilder(os.Getenv("KESSEL_ENDPOINT")).
Authenticated(kesselgrpc.OAuth2CallCredentials(&oauthCreds), tlsCreds).
Build()
if err != nil {
return nil, nil, fmt.Errorf("build client: %w", err)
}
return client, func() { conn.Close() }, nil
}

//#endregion

//#region report-on-create
func (s *TaskService) CreateTask(ctx context.Context, req *CreateTaskRequest) (*CreateTaskResponse, error) {
// --- Your existing service logic ---
task, err := s.db.InsertTask(ctx, req)
if err != nil {
return nil, status.Errorf(codes.Internal, "insert task: %v", err)
}

// --- Report to Kessel ---
_, err = s.kessel.ReportResource(ctx, &v1beta2.ReportResourceRequest{
Type: "task",
ReporterType: "TASKMANAGER",
ReporterInstanceId: s.instanceID,
Representations: &v1beta2.ResourceRepresentations{
Metadata: &v1beta2.RepresentationMetadata{
LocalResourceId: task.ID,
ApiHref: fmt.Sprintf("%s/api/tasks/%s", s.baseURL, task.ID),
},
Common: &structpb.Struct{
Fields: map[string]*structpb.Value{
"workspace_id": structpb.NewStringValue(task.WorkspaceID),
},
},
Reporter: &structpb.Struct{
Fields: map[string]*structpb.Value{
"title": structpb.NewStringValue(task.Title),
"status": structpb.NewStringValue(task.Status),
},
},
},
})
if err != nil {
// Log the error but don't fail the request.
// The resource exists in your database; Kessel will catch up
// on the next report or through a reconciliation process.
log.Printf("kessel: failed to report task %s: %v", task.ID, err)
}

return &CreateTaskResponse{Task: task}, nil
}

//#endregion

//#region check-middleware
func KesselAuthInterceptor(client v1beta2.KesselInventoryServiceClient, resourceType, reporterType, relation string) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.PermissionDenied, "missing metadata")
}

userIDs := md.Get("x-user-id")
if len(userIDs) == 0 {
return nil, status.Error(codes.PermissionDenied, "missing user identity")
}
userID := userIDs[0]

resourceID := extractResourceID(req)

resp, err := client.Check(ctx, &v1beta2.CheckRequest{
Object: &v1beta2.ResourceReference{
ResourceType: resourceType,
ResourceId: resourceID,
Reporter: &v1beta2.ReporterReference{Type: reporterType},
},
Relation: relation,
Subject: &v1beta2.SubjectReference{
Resource: &v1beta2.ResourceReference{
ResourceType: "principal",
ResourceId: userID,
Reporter: &v1beta2.ReporterReference{Type: "rbac"},
},
},
})

if err != nil {
if st, ok := status.FromError(err); ok && st.Code() == codes.Unavailable {
return nil, status.Error(codes.Unavailable, "authorization service unavailable")
}
return nil, status.Error(codes.PermissionDenied, "forbidden")
}

if resp.Allowed != v1beta2.Allowed_ALLOWED_TRUE {
return nil, status.Error(codes.PermissionDenied, "forbidden")
}

return handler(ctx, req)
}
}

// Wire it up when creating the gRPC server:
//
// viewAuth := KesselAuthInterceptor(kesselClient, "task", "TASKMANAGER", "view")
// editAuth := KesselAuthInterceptor(kesselClient, "task", "TASKMANAGER", "edit")
//
// server := grpc.NewServer(
// grpc.ChainUnaryInterceptor(viewAuth),
// )
//#endregion

// Type stubs for compilation context.
type TaskService struct {
db interface{ InsertTask(context.Context, *CreateTaskRequest) (Task, error) }
kessel v1beta2.KesselInventoryServiceClient
instanceID string
baseURL string
}
type CreateTaskRequest struct{}
type CreateTaskResponse struct{ Task Task }
type Task struct {
ID string
WorkspaceID string
Title string
Status string
}

func extractResourceID(req interface{}) string { return "" }
func main() {}
Loading
Loading