Getting started with SpiceDB in .NET

nathan sains
KPMG UK Engineering
11 min readJun 27, 2023

Introduction

SpiceDB is an open-source permissions database used for centralising permissions across your application. It’s based on the ReBAC model which was popularised by Google in 2019 with the release of their Zanzibar white-paper. ReBAC (Relationship Based Access Control) is a strategy for modelling your permissions systems as a graph of relationships between entities. It’s particularly useful for modelling applications which require Fine Grained Authorisation (FGA) of resources.

SpiceDB itself is a golang application which exposes a gRPC server for interacting with your permissions system. It can be used with a variety of backing stores for storing your permissions such as PostgreSQL, CockroachDB, and Spanner. A typical application setup might look something like the below where we have multiple applications interacting with a cluster of SpiceDB nodes.

Example application architecture when using SpiceDB as a centralised permissions database

Some of the benefits of using SpiceDB include:

  • Decouples permissions logic from your application code
  • Powerful and expressive schema language for expressing your permissions model
  • Low-latency access to your permissions — SpiceDB breaks down permission checks and dispatches them across nodes. Sub-checks are dispatched to the node most likely to have the response cached meaning we can achieve low-latency responses.

This blog post is split into three parts:

If you’re familiar with SpiceDB feel free to skip the first two sections and jump straight to using SpiceDB from a .NET application.

Part 1 — Setting up SpiceDB

Before we can start, we’ll need to have an instance of SpiceDB running locally. You can find the most up-to-date instructions for installing SpiceDB here.

For this demo we’ll just spin up a docker container using the command below. This starts a SpiceDB server and maps port 50051 to the host which is used to handle gRPC requests.

docker run -d --rm \
-p 50051:50051 \
--name spicedb \
authzed/spicedb serve \
--grpc-preshared-key "somerandomkeyhere"

# You can check the above command worked by running:
docker ps | awk 'NR==1 || /spicedb/'

Note: be sure to take note of the grpc-preshared-key you used for later in the walkthrough.

Now that we have SpiceDB running locally we can interact with it using the Zed CLI. The Zed CLI is a command line tool we can use for interacting with SpiceDB until we’ve created our application. We’ll use it to create our schema and then to create some test data. You can find the most up-to-date instructions for installing the Zed CLI here. On a mac the simplest option is to use: brew install authzed/tap/zed

Once installed we can connect to our local instance with the command below. For local development we can use the--insecure flag to connect over plaintext. Be sure to replace the grpc-preshared-key with the one you used in the previous step.

zed context set dev localhost:50051 somerandomkeyhere --insecure

# You can check the above command worked by running:
zed version

Note: If the output of zed versionshows the server version as unknown then your CLI was unable to connect so you may need to double check some values in the previous steps such as the grpc-preshared-key or the port your instance is running on.

Part 2 — Modelling a permissions schema

In order to write and check permissions from our application we’ll need to create a SpiceDB schema. We’ll be using a simple schema in this walkthrough but you can use the AuthZed playground to help you create something more complex.

Before we start writing the schema, we should cover some common terminology used in SpiceDB:

  • definition — typically represents an entity in your system. For example, you might have a user and a group.
  • relation — a relation describes how two definitions are related. For example, a user might have the member relation to a group.
  • permission — a permission typically represents an action in your system. For example, a user with the admin relation to a group might have the can_add_member permission. How the permissions map to relations is determined in the schema.

Here’s an example of a schema in which we have two types of entity, user and group. Users can either have the admin or member relation to a group. The type of relation a user has to a group determines what permissions they have.

You’ll see later how we can use permissions to issue a check request. Essentially, the logic for how permissions relate to entities within our system is all defined in our schema. This means that when we check a permission from our application it just needs to pass in the permission name as string, and not any complicated mappings of relations.

Add the schema below to the root of your solution as schema.zed

definition user {}

definition group {
relation admin: user
relation member: user

permission can_view_group = admin + member
permission can_add_member = admin
}

Note: The + syntax in the can_view_group permission denotes that either admins or members of the group have the permission. If we wanted to specify that a user needs both relations in order to have the permission one would use the & notation. A full reference to the schema language can be found here.

Once we have our schema, we can write it to SpiceDB using the Zed CLI. If it works you should see your schema printed after running the read command.

zed schema write ./schema.zed

# You can check the above command worked by running:
zed schema read

Now that our schema is written we can seed some test data using the Zed CLI. We’ll create a couple of users bob and alice and add them as members of a group called devs.

zed relationship touch group:devs admin user:bob
zed relationship touch group:devs member user:alice

In SpiceDB relationships are represented as relation tuples. Each tuple contains a resource, a relation and a subject. In our case the resource is the name of a group, the relation is either admin or member, and the subject is the name of a user.

You could picture the relations above visually as a graph like the one below. Although we haven’t modelled it in our schema, you can see how we could add other types of entities and model relationships between them. For example, all users of the group managers below would automatically have access to some_resource:1.

A visual representation of some relation tuples within SpiceDB. Our two users, bob and alice, have relations to groups and the groups may have relations to other resources.

To check our schema is working correctly we can issue a couple of check requests. As alice is only a member we expect them to have the can_view_group permission but not the can_add_member permission. Conversely, as bob is an admin we expect them to have both permissions.

zed permission check group:devs can_view_group user:alice
# output: true

zed permission check group:devs can_add_member user:alice
# output: false

Note: In a realistic application you’d probably use something that uniquely identifies each user such as a userID or SubjectID from your IdP.

Part 3 — Creating a .NET application

We’re now ready to create our application. We’ll create ourselves a blank .NET solution and add a boilerplate API project to it.

# create solution
dotnet new sln --name SpiceDbStarter

# create blank api project
dotnet new webapi --name Api --kestrelHttpPort 5000 --use-minimal-apis

# add the project to the solution
dotnet sln add ./Api/Api.csproj

# run the api -> open on http://localhost:5000/swagger
dotnet run --project Api

If the above works, you should see a swagger document at http://localhost:5000/swagger with a single WeatherForecasts endpoint. Feel free to remove anything related to WeatherForecasts in Api/Program.cs as we’ll add our own endpoint later.

We can now add our SpiceDB client to our solution. Unfortunately, at the time of writing, SpiceDB does not publish their own .NET client so we’ll have to create our own using the protobuf definitions. Firstly, we’ll add a new class library to contain our gRPC client.

# add the class library
dotnet new classlib --name SpiceDbClient

# add the project to the solution
dotnet sln add ./SpiceDbClient/SpiceDbClient.csproj

# add references to protobuf and grpc nuget packages
dotnet add SpiceDbClient package Google.Protobuf
dotnet add SpiceDbClient package Grpc.Core
dotnet add SpiceDbClient package Grpc.Net.ClientFactory

Next, we’ll need to use the protobuf definitions to autogenerate the gRPC client. AuthZed publishes the SpiceDB protobuf definitions here on buf.build. This also serves as useful documentation to understand the API.

Note: If you aren’t familiar with Protocol Buffers, they’re essentially a language-agnostic format for serialising / deserialising messages. In this example we use code-generation tools on the protobuf files in order to produce a typed gRPC client for our .NET application.

To generate the client from here we’ll need to install the buf CLI, installation instructions can be found here. On a mac the simplest option is to use brew install bufbuild/buf/buf.

To instruct buf how to generate our code we need a config file. Our buf configuration file will look something like this, add it to the root of your project as buf.gen.yaml.

version: "v1"
plugins:
- plugin: buf.build/grpc/csharp
out: gen
- plugin: buf.build/protocolbuffers/csharp
out: gen

At this point I was hoping I’d be able to generate the SDK with the single command: buf generate buf.build/authzed/api. However, when running this command I get an error which seems to indicate a clash in the protobuf definitions. I assume this is because two or more of SpiceDB’s dependencies have a conflicting filename and the protoc autogeneration is trying to put them all in the same directory.

To work around this, we can just generate all of the dependencies individually and specify an output folder for each one.

It’s slightly more manual and we’ll need to be careful to check for any dependency changes each time we generate a new client. Luckily, we can find the latest commit hashes for each dependency in SpiceDB’s buf.lock file. Depending on when you read this, you might need to substitute the commit hashes below for the latest ones.

# 1. Generate the spicedb client
buf generate buf.build/authzed/api --path authzed/api/v1/ \
-o ./SpiceDbClient/generated/authzed.api

# 2. Generate the dependencies
# 2.1 protoc-gen-validate
buf generate buf.build/envoyproxy/protoc-gen-validate:45685e052c7e406b9fbd441fc7a568a5 \
-o ./SpiceDbClient/generated/envoyproxy.protoc-gen-validate

# 2.2 googleapis
buf generate buf.build/googleapis/googleapis:62f35d8aed1149c291d606d958a7ce32 \
--path google/api/annotations.proto \
--path google/api/http.proto \
-o ./SpiceDbClient/generated/googleapis.googleapis

# 2.3 grpc-gateway
buf generate buf.build/grpc-ecosystem/grpc-gateway:bc28b723cd774c32b6fbc77621518765 \
-o ./SpiceDbClient/generated/grpc-ecosystem.grpc-gateway

Under ./SpiceDbClient/generated/ you should now see the generated client code. Double check it compiles with a dotnet build, if it works then we’re good to continue

Your generated code should look something like the folder structure below:

Output folder structure of the buf.build code code generation for our SpiceDB gRPC client.

Now that we have our client, we can add a reference to it in our API project

dotnet add Api reference SpiceDbClient

Next, we’ll add the gRPC clients to the DI container in Api/Program.cs. Add the code below with the other service registrations (i.e. add this below WebApplication.CreateBuilder(args), but above var app = builder.Build())

using Grpc.Core;
using Grpc.Net.Client;
using Grpc.Net.ClientFactory;

var builder = WebApplication.CreateBuilder(args);

// ... code omitted for brevity

void ConfigureSpiceDbGrpcClient(IServiceProvider provider, GrpcClientFactoryOptions options)
{
options.Address = new Uri("http://localhost:50051");
}

void ConfigureSpiceDbChannel(IServiceProvider provider, GrpcChannelOptions options)
{
var credentials = CallCredentials.FromInterceptor((context, metadata) =>
{
const string token = "somerandomkeyhere"; // NOTE - this will need to come from config eventually
metadata.Add("Authorization", $"Bearer {token}");
return Task.CompletedTask;
});

// NOTE - we're using Insecure credentials only for this demo so that we don't need to setup TLS
options.UnsafeUseInsecureChannelCallCredentials = true;
options.Credentials = ChannelCredentials.Create(ChannelCredentials.Insecure, credentials);
}

builder.Services.AddGrpcClient<PermissionsService.PermissionsServiceClient>(ConfigureSpiceDbGrpcClient)
.ConfigureChannel(ConfigureSpiceDbChannel);

var app = builder.Build();

// ... code omitted for brevity

Note: Remember to replace the gRPC token above with the one from earlier. In a proper application make sure you remember to move this out into config. Also note that we’re using CallCredentials.Insecure for demo purposes.

With the code above we’ve now added the PermissionsServiceClient to the DI container so we can use it later. Now, we’ll add a new endpoint to our API and perform a permission check.

Add this just above app.Run() in Api/Program.cs

using Authzed.Api.V1;

// ... code omitted for brevity

// hardcode some groups for now, in a real app these might come from a db
var groups = new[]
{
new { Name = "devs" },
new { Name = "qa" },
new { Name = "designers" },
};

// adds a new endpoint for getting a single group
app.MapGet("/groups/{groupName}", async (
[FromRoute] string groupName,
[FromServices] PermissionsService.PermissionsServiceClient client,
[FromHeader] string userName) =>
{
var group = groups.FirstOrDefault(g => g.Name == groupName);
if (group is null)
{
return Results.NotFound();
}

var permission = await client.CheckPermissionAsync(new CheckPermissionRequest
{
Consistency = new Consistency
{
FullyConsistent = true
},
Subject = new SubjectReference
{
Object = new ObjectReference
{
ObjectType = "user",
ObjectId = userName
}
},
// Note - This string needs to match a permission from our schema
Permission = "can_view_group",
Resource = new ObjectReference
{
ObjectType = "group",
ObjectId = groupName
}
});

if (permission.Permissionship != CheckPermissionResponse.Types.Permissionship.HasPermission)
{
return Results.StatusCode(403);
}

return Results.Ok(group);
})
.WithName("GetGroup")
.WithTags("Groups")
.WithOpenApi();

app.Run();

There’s a fair amount to unpack here. Firstly, we’ve added a new endpoint to return a singular group. Our method takes a few parameters:

  • groupName: identifies a group — this will match the group names we created inside SpiceDB earlier
  • client: this is the spiceDB permissions client that we just added to the DI container. The PermissionsServiceClient allows us to issue check requests as well as write requests to SpiceDB.
  • userName: this is a header that we’ll pass in to identify the user making the request. In a real system you’ll obviously want to take this from a claim or some other secure source, but for this demo we can pass in the name of the user along with the request.

The first thing we do is check if the group exists. Next, we issue a check permission request to SpiceDB. The request here has the same composition to the one earlier from the Zed CLI. Every check permission request must have a subject, permission, and a resource.

You’ll also notice we included an API consistency. SpiceDB includes several consistency options which can be passed on the request. The default is fully_consistent, but you can read about the other consistency options here

If you try running the API project again and navigating to the swagger endpoint, you’ll be able to try a request. Try using the groupName devs and the userName bob to see a success response. Next, if we try with some other userName that doesn’t exist in SpiceDB such as james we should see a 403 response where the permission check has failed.

An example request to our get group endpoint which results in a Forbidden response as no relation exists for the user “James” and the group “devs”

Conclusion & Next Steps

At this point we have a working .NET application that can call SpiceDB to check permissions.

We won’t be able to cover any more in this walkthrough but there are a few notable steps that you could explore adding to your application:

  • Writing Relationships: You can use the same client we used earlier to write relationships between objects. For example, if we want to add or remove a user from a group.
  • Adding the other SpiceDB clients: We added the PermissionsServiceClient earlier, but you’ll notice there are several others we can add. For example, the WatchServiceClient and the SchemaServiceClient.
  • Caveats: These could probably be the subject of their own blog post — but caveats allow us to add additional context to our permission checks. For example, if we wanted to add a caveat that only allows users to read a group if the current day is a Tuesday. Caveats allow us to capture context-dependent logic in our permission checks that couldn’t otherwise be represented by simple relations between objects.
  • Integration Testing: SpiceDB ships with a built-in integration testing mode that allows you to test against a real SpiceDB instance. In this mode each unique grpc-preshared-key you pass to SpiceDB will create a sandboxed in-memory partition which is particularly handy if we want to write granular unit tests against our schema using some test data

--

--