Biscuit snapshots

Posted November 19, 2023 by clementd ‐ 4 min read

What are biscuit snapshots, how do they work and how can they be useful

One of the defining features in biscuit is the common language for authorization policies. Along with the cryptographic constructs used in tokens, it is what allows offline attuenation.

A common language for policies means that we can have a standardized serialization format, which in turn means it can be embedded in a token, which gives us offline attenuation. Neat!

It turns out that being able to serialize authorization policies gives us another benefit: it is possible to take a snapshot of an authorization process and save it for later. That's what we'll talk about today.

Authorizer snapshots

In biscuit-rust and most biscuit libraries, the authorization process is carried out through an Authorizer value.

An Authorizer is created from a biscuit token, along with facts, rules, checks, and policies added by the authorizing party.

Once all this has been provided, the Authorizer runs datalog evaluation (it repeatedly generates new datalog facts from rules unless no new facts can be generated). Once this is done, checks and policies are evaluated and are used to compute the authorization result (all checks have to pass, and the first policy to match must be an allow policy). The Authorizer makes sure these two steps are carried out in a timely fashion by aborting after a specified timeout, if too many facts are generated, or after a specific amount of iterations. This is crucial to make sure authorization does not become a DoS target.

The good news is that an Authorizer only contains serializable data, and as such can be stored, logged, or displayed.

Here is an example of creating a snapshot with biscuit-rust.

let mut authorizer = authorizer!(
  r#"time({now});
  resource("/file1.txt");
  operation("read");
  check if user($user);
  allow if right("/file1.txt", "read");
  "#,
  now = SystemTime::now(),
);
authorizer.add_token(biscuit);
let result = authorizer.authorize();
println!("{}", authorizer.to_base64_snapshot());

This will give you something like:

 CgkI6AcQZBjAhD0Q2YkBGvMBCAQSCi9maWxlMS50eHQSBDEyMzQiRBADGgkKBwgKEgMYgQgaDQoLCAQSAxiACBICGAAqJgokCgIIGxIGCAUSAggFGhYKBAoCCAUKCAoGIIDEpKsGCgQaAggAKjUQAxoJCgcIAhIDGIAIGggKBggDEgIYABoMCgoIBRIGILCX3aoGKg4KDAoCCBsSBggKEgIICjIVChEKAggbEgsIBBIDGIAIEgIYABAAOicKAgoAEggKBggDEgIYABIJCgcIAhIDGIAIEgwKCggFEgYgsJfdqgY6HgoCEAASDQoLCAQSAxiACBICGAASCQoHCAoSAxiBCEAA 

Once you have that, you can inspect it with the CLI:

$ echo "CgkI6AcQZBjAhD0Q2YkBGvMBCAQSCi9maWxlMS50eHQSBDEyMzQiRBADGgkKBwgKEgMYgQgaDQoLCAQSAxiACBICGAAqJgokCgIIGxIGCAUSAggFGhYKBAoCCAUKCAoGIIDEpKsGCgQaAggAKjUQAxoJCgcIAhIDGIAIGggKBggDEgIYABoMCgoIBRIGILCX3aoGKg4KDAoCCBsSBggKEgIICjIVChEKAggbEgsIBBIDGIAIEgIYABAAOicKAgoAEggKBggDEgIYABIJCgcIAhIDGIAIEgwKCggFEgYgsJfdqgY6HgoCEAASDQoLCAQSAxiACBICGAASCQoHCAoSAxiBCEAA" \
  | biscuit inspect-snapshot -

// Facts:
// origin: 0
right("/file1.txt", "read");
user("1234");
// origin: authorizer
operation("read");
resource("/file1.txt");
time(2023-11-17T11:17:04Z);

// Checks:
// origin: authorizer
check if user($user);
// origin: 0
check if time($time), $time < 2023-12-01T00:00:00Z;

// Policies:
allow if right("/file1.txt", "read");

⏱️ Execution time: 17μs (0 iterations)
🙈 Datalog check skipped 🛡️

Or directly with the web-based snapshot inspector:

Here you can see the whole authorization context, as well as interesting metadata such as the time taken by the authorization process (17μs, not too bad) and the number of iterations needed by fact generation (here, 0 as there are no rules).

Snapshots use cases

Auditing & debugging

Being able to inspect the full authorization context after the fact feels a bit like a superpower. You can confidently say why a request was granted or denied after the fact. This can save you hours of work when trying to debug a gnarly authorization issues, instead of trying to modify your access policies until something works.

I have found snapshots to be immensely valuable when working on complex authorization logic. Instead of using a debugger or putting println!() calls everywhere, I just printed an authorizer snapshot and inspected it interactively with biscuit-cli. For instance, biscuit-cli lets me run queries on snapshots, which helped me easily detect typos or test predicates.

A similar use-case is auditing access. For highly sensitive operations, you might want to keep track of who is accessing resources, and why they are allowed to. Snapshots are a perfect use-case for that.

Resumable execution

Snapshots allow separating the authorization process in several steps: first you create an authorizer from a biscuit token, and then pass the authorizer around (serialized through a snapshot), to finally resume authorization somewhere else. While doing it in one step is better in most cases, some software stacks can be overly restrictive and force a separate authentication step (verify biscuit signatures) before authorization (evaluate datalog policies).

Tooling support

Saving and loading snapshots is available in biscuit-rust and biscuit-python.

biscuit-cli also lets you save a snapshot (biscuit inspect --dump-snapshot-to) and inspect a snapshot (biscuit inspect-snapshot), with optional authorizer code and queries. biscuit-web-components provides a <bc-snapshot-printer> component, which allows inspecting and querying snapshot.