Working with datalog

Biscuit uses a custom logic language both for token contents and authorization rules.

biscuit-python strives to make it easy to embed datalog snippets in python programs.

Building datalog snippets through string manipulation is dangerous, do not do it. Biscuit-python provides safe way to exchange values between python and datalog.

Parameter interpolation

Datalog parameters provide a way to inject dynamic values in datalog without manipulating strings directly. This avoids datalog injections, which are similar to SQL injections.

Parameters are names enclosed in curly brackets: {param}. They can appear everywhere a value is allowed: in predicate terms, or within expressions.

All datalog builders (BiscuitBuilder, BlockBuilder, Authorizer, Fact, Rule, Check, Policy) support parameters: a dictionnary containing params values must be supplied along the datalog code:

>>> from biscuit_auth import BiscuitBuilder, BlockBuilder, Authorizer, Check, Fact, Rule, Policy, PublicKey
>>> Fact("user({user})", { 'user': 1234})
user(1234)
>>> Rule("head($u, {val}) <- body($u), $u == {val}", { 'val': "abcd"})
head($u, "abcd") <- body($u), $u == "abcd"
>>> BiscuitBuilder("""
... check if right($r), {rights}.contains($r);
... """, {'rights': {'read', 'write'}})
// no root key id set
check if right($r), ["read", "write"].contains($r);

Rules, checks and policies can also contain parameters for public keys. Those are specified in a separate dictionnary:

>>> BlockBuilder("""
... check if admin({user}) trusting {rights_service_pubkey}
... """,
... {'user': "abcd" },
... {'rights_service_pubkey': PublicKey.from_hex("9e124fbb46ff99a87219aef4b09f4f6c3b7fd96b7bd279e38af3ef429a101c69") })
check if admin("abcd") trusting ed25519/9e124fbb46ff99a87219aef4b09f4f6c3b7fd96b7bd279e38af3ef429a101c69;

Whole datalog snippets

BiscuitBuilder(), BlockBuilder() and Authorizer() accept whole datalog snippets, with statements separated by semicolons

>>> BiscuitBuilder("""
... user({user_id});
... check if operation("read");
... """, { 'user_id': 1234 })
// no root key id set
user(1234);
check if operation("read");

Individual datalog elements

While using BiscuitBuilder(), BlockBuilder(), Authorizer() works well for static datalog snippets, it is sometimes necessary to dynamically add facts, rules or policies (for instance from inside a loop or an if block).

Individual facts, rules, checks or policies can be built and added to builders like this:

>>> resources = ["file1", "file2"]
>>> builder = BiscuitBuilder("")
>>> for r in resources:
...   builder.add_fact(Fact("""right({r}, "read")""", { 'r': r}))
>>> builder
// no root key id set
right("file1", "read");
right("file2", "read");

In addition to add_fact, there are add_rule, add_check, and add_policy. In addition to Fact(), there are Rule(), Check(), and Policy().

Semicolons are not part of individual statements:

>>> Fact("user(1234)")
user(1234)
>>> Fact("user(1234);")
Traceback (most recent call last):
    ...
biscuit_auth.DataLogError: error generating Datalog: datalog parsing error: ParseErrors { errors: [ParseError { input: ";", message: Some("unexpected trailing data after fact: ';'") }] }

Supported types

Datalog supports 8 types of values (integers, strings, booleans, datetime, bytearray, set (sets cannot be nested). biscuit-python supports all of those:

Types correspondence

Python type

Datalog type

int

integer

str

string

aware datetime

date

bytes, list<int>

bytes

bool

bool

set

set

Warning

Naive dates are not supported, only aware dates: the timezone must be explictly specified (datetimes are stored as UTC timestamp with no explicit timezone information, so using UTC in python will make things simpler).

>>> from datetime import datetime, timezone
>>> now = Fact("time({now})", {'now': datetime.now(tz = timezone.utc)})
>>> Fact("time({now})", {'now': datetime(2023, 6, 9, tzinfo = timezone.utc)})
time(2023-06-09T00:00:00Z)
>>> Fact("bytes({bytes})", {'bytes': b'\xaa\xbb\xff'})
bytes(hex:aabbff)
>>> Fact("bytes({bytes})", {'bytes': [0xaa, 0xbb, 255]})
bytes(hex:aabbff)
>>> Fact("set({set})", {'set': {0, True, "ab", b'\xaa'}})
set([0, "ab", hex:aa, true])

Inspecting datalog values

Terms of a fact can be extracted to python values.o

>>> fact = Fact("""fact("abc", 123, hex:aa, 2023-06-09T00:00:00Z, true)""")
>>> fact.name
'fact'
>>> fact.terms
['abc', 123, [170], datetime.datetime(2023, 6, 9, 0, 0, tzinfo=datetime.timezone.utc), True]

Warning

Extracting sets is not supported yet.

>>> Fact("fact([123])").terms
Traceback (most recent call last):
    ...
pyo3_runtime.PanicException: not yet implemented