Getting started with SpiceDB in .NET
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.
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:
- Part 1 — Setting up SpiceDB
- Part 2 — Modelling a permissions schema
- Part 3 — Creating a .NET application
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 version
shows the server version asunknown
then your CLI was unable to connect so you may need to double check some values in the previous steps such as thegrpc-preshared-key
or theport
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 auser
and agroup
.relation
— a relation describes how two definitions are related. For example, auser
might have themember
relation to agroup
.permission
— a permission typically represents an action in your system. For example, auser
with theadmin
relation to agroup
might have thecan_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 thecan_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
.
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:
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 earlierclient
: 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.
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 theSchemaServiceClient
. - 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