Skip to content

Commit 1514f11

Browse files
authored
Update readme & grammar documentation (#125)
* doc: update readme with up-to-date code examples * doc: update grammar reference It referenced biscuit v0 / biscuit v1 syntax
1 parent 53ef352 commit 1514f11

File tree

2 files changed

+107
-53
lines changed

2 files changed

+107
-53
lines changed

README.md

+53-26
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,38 @@ biscuit-go is an implementation of [Biscuit](https://github.com/biscuit-auth/bis
1515
```go
1616
rng := rand.Reader
1717
publicRoot, privateRoot, _ := ed25519.GenerateKey(rng)
18-
builder := biscuit.NewBuilder(privateRoot)
1918

20-
fact1, err := parser.FromStringFact(`right("/a/file1.txt", "read")`)
21-
if err != nil {
22-
panic(fmt.Errorf("failed to parse authority facts: %v", err))
23-
}
19+
authority, err := parser.FromStringBlockWithParams(`
20+
right("/a/file1.txt", {read});
21+
right("/a/file1.txt", {write});
22+
right("/a/file2.txt", {read});
23+
right("/a/file3.txt", {write});
24+
`, map[string]biscuit.Term{"read": biscuit.String("read"), "write": biscuit.String("write")})
2425

25-
err := builder.AddAuthorityFact(fact1)
2626
if err != nil {
27-
panic(fmt.Errorf("failed to add authority facts: %v", err))
27+
panic(fmt.Errorf("failed to parse authority block: %v", err))
2828
}
2929

30-
// ... add more authority facts, rules, caveats...
30+
builder := biscuit.NewBuilder(privateRoot)
31+
builder.AddBlock(authority)
3132

3233
b, err := builder.Build()
3334
if err != nil {
34-
panic(fmt.Errorf("failed to build biscuit: %v", err))
35+
panic(fmt.Errorf("failed to build biscuit: %v", err))
3536
}
37+
3638
token, err := b.Serialize()
3739
if err != nil {
38-
panic(fmt.Errorf("failed to serialize biscuit: %v", err))
40+
panic(fmt.Errorf("failed to serialize biscuit: %v", err))
3941
}
4042

4143
// token is now a []byte, ready to be shared
42-
// If you want a base64 encoded token, do it like this
44+
// The biscuit spec mandates the use of URL-safe base64 encoding for textual representation:
4345
fmt.Println(base64.URLEncoding.EncodeToString(token))
4446
```
4547

4648
#### Attenuate a biscuit
49+
4750
```go
4851
b, err = biscuit.Unmarshal(token)
4952
if err != nil {
@@ -52,9 +55,13 @@ if err != nil {
5255

5356
// Attenuate the biscuit by appending a new block to it
5457
blockBuilder := b.CreateBlock()
55-
blockBuilder.AddFact(biscuit.Fact{/* ... */})
56-
57-
// ... add more facts, rules, caveats...
58+
block, err := parser.FromStringBlockWithParams(`
59+
check if resource($file), operation($permission), [{read}].contains($permission);`,
60+
map[string]biscuit.Term{"read": biscuit.String("read")})
61+
if err != nil {
62+
panic(fmt.Errorf("failed to parse block: %v", err))
63+
}
64+
blockBuilder.AddBlock(block)
5865

5966
attenuatedBiscuit, err := b.Append(rng, blockBuilder.Build())
6067
if err != nil {
@@ -65,7 +72,7 @@ if err != nil {
6572
panic(fmt.Errorf("failed to serialize biscuit: %v", err))
6673
}
6774

68-
// token is now a []byte attenuation of the original token, and ready to be shared
75+
// attenuatedToken is a []byte, representing an attenuated token
6976
```
7077

7178
#### Verify a biscuit
@@ -81,16 +88,15 @@ if err != nil {
8188
panic(fmt.Errorf("failed to verify token and create authorizer: %v", err))
8289
}
8390

84-
fact1, err := parser.FromStringFact(`resource("/a/file1.txt")`)
91+
authorizerContents, err := parser.FromStringAuthorizerWithParams(`
92+
resource({res});
93+
operation({op});
94+
allow if right({res}, {op});
95+
`, map[string]biscuit.Term{"res": biscuit.String("/a/file1.txt"), "op": biscuit.String("read")})
8596
if err != nil {
86-
panic(fmt.Errorf("failed to parse authority facts: %v", err))
97+
panic(fmt.Errorf("failed to parse authorizer: %v", err))
8798
}
88-
89-
auhorizer.AddFact(fact1)
90-
91-
// ... add more ambient facts, rules, caveats...
92-
93-
authorizer.AddPolicy(biscuit.DefaultAllowPolicy)
99+
authorizer.AddAuthorizer(authorizerContents)
94100

95101
if err := authorizer.Authorize(); err != nil {
96102
fmt.Printf("failed authorizing token: %v\n", err)
@@ -101,18 +107,39 @@ if err := authorizer.Authorize(); err != nil {
101107

102108
### Using biscuit-go grammar
103109

104-
To ease adding facts, rules, or caveats, a simple grammar and a parser are available, allowing to declare biscuit elements as plain strings. See [GRAMMAR reference](./parser/GRAMMAR.md) for the complete syntax.
110+
biscuit-go provides a datalog parser, allowing to input datalog elements as plain strings, along with support for parameter substitution.
111+
112+
See [GRAMMAR reference](./parser/GRAMMAR.md) for the complete syntax.
113+
114+
The parsers supports parsing whole blocks (containing several facts, rules and checks), whole authorizers (containing several facts, rules, checks and policies), as well as individual facts, rules, checks and policies. Parsing and adding elements individually is especially useful when doing so from inside a loop.
115+
116+
The `parser` module provides convenient helpers for parsing a string into datalog elements (`FromStringFact`, `FromStringRule`, `FromStringCheck`, `FromStringPolicy`, `FromStringBlock`, `FromStringAuthorizer`, for static datalog snippets, and their counterparts allowing parameter substitution: `FromStringFactWithParams`, `FromStringRuleWithParams`, `FromStringCheckWithParams`, `FromStringPolicyWithParams`, `FromStringBlockWithParams`, `FromStringAuthorizerWithParams`).
117+
118+
#### Panic on parsing errors
119+
120+
In most cases, `FromString*` functions will let you handle errors. If you do not wish to handle errors and instead crash on errors (for instance in one-off scripts), it can be done by first creating a parser instance, and using the `panic`-y functions:
105121

106122
```go
107123
p := parser.New()
108-
b.AddFact(p.Must().Fact(`resource("/a/file1.txt")`))
124+
b := biscuit.NewBuilder(privateRoot)
125+
126+
b.AddBlock(p.Must().Block(`
127+
right("/a/file1.txt", {read});
128+
right("/a/file1.txt", {write});
129+
right("/a/file2.txt", {read});
130+
right("/a/file3.txt", {write});
131+
`, map[string]biscuit.Term{"read": biscuit.String("read"), "write": biscuit.String("write")}))
132+
133+
b.AddFact(p.Must().Fact(`resource({res})`, map[string]biscuit.Term{"res": biscuit.String("/a/file1.txt")}))
109134
b.AddRule(p.Must().Rule(`
110135
can_read($file)
111136
<- resource($file)
112137
$file.starts_with("/a/")
113-
`))
138+
`, nil))
114139
```
115140

141+
Do note that these helpers take two arguments: a datalog snippet and a parameters map. If the datalog snippet does not contain parameters, `nil` can be passed as the second argument.
142+
116143
## Examples
117144

118145
- [example_test.go](./example_test.go) for a simple use case

parser/GRAMMAR.md

+54-27
Original file line numberDiff line numberDiff line change
@@ -4,78 +4,105 @@ This document describes the currently supported Datalog grammar.
44

55
## Term
66

7-
Represents a Datalog type, can be one of: symbol, variable, integer, string, date, bytes, boolean, or set.
7+
Represents a Datalog type, can be one of: parameter, variable, integer, string, date, bytes, boolean, or set.
88

9-
- symbol is prefixed with a `#` sign followed by text, e.g. `#read`
9+
- parameter is delimited by curly brackets: `{param}`. Those are replaced by actual values before evaluation.
1010
- variable is prefixed with a `$` sign followed by a string or an unsigned 32bit base-10 integer, e.g. `$0` or `$variable1`
1111
- integer is any base-10 int64
1212
- string is any utf8 character sequence, between double quotes, e.g. `"/path/to/file.txt"`
13-
- date is RFC3339 encoded, e.g. `2006-01-02T15:04:05Z07:00`
13+
- date is RFC3339 encoded, e.g. `2006-01-02T15:04:05Z`
1414
- bytes is an hexadecimal encoded string, prefixed with a `hex:` sequence
1515
- boolean is either `true` or `false`
16-
- set is a sequence of any of the above types, except variable, between brackets, e.g. `[#read, #write, #update, "file1", "file2"]`
16+
- set is a sequence of any of the above types, except variable, between brackets, e.g. `["file1", "file2"]` (sets cannot be nested)
1717

1818
## Predicate
1919

20-
A predicate is a list of terms, grouped under a name in the form `Name(Term0, Term1, ..., TermN)` , e.g. `parent(#a, #b)`.
20+
A predicate is a list of terms, grouped under a name in the form `Name(Term0, Term1, ..., TermN)` , e.g. `parent("a", "b")`.
2121

2222
## Constraints
2323

2424
Constraints allows performing checks on a variable, below is the list of available operations by type and their expected format.
2525

26+
### Boolean
27+
28+
- Equal: `$b == true`
29+
- Negation: `!$b`
30+
- And / Or: `$b || $c && $d`
31+
2632
### Integer
2733

2834
- Equal: `$i == 1`
2935
- Greater than: `$i > 1`
3036
- Greater than or equal: `$i >= 1`
3137
- Less than: `$i < 1`
3238
- Less than or equal: `$i <= 1`
33-
- In: `$i in [1, 2, 3]`
34-
- Not in: `$i not in [1, 2, 3]`
39+
- Arithmetic (`*`, `/`, `+`, `-`)
3540

3641
### String
3742

3843
- Equal: `$s == "abc"`
39-
- Starts with: `prefix($s, "abc")`
40-
- Ends with: `suffix($s, "abc")`
41-
- Regular expression: `match($s, "^abc\s+def$") `
42-
- In: `$s in ["abc", "def"]`
43-
- Not in: `$s not in ["abc", "def"]`
44+
- Starts with: `$s.starts_with("abc")`
45+
- Ends with: `$s.ends_with("abc")`
46+
- Regular expression: `$s.matches("^abc\s+def$") `
47+
- Contains: `$s.contains("abc")`
48+
- Length: `$s.length()`
4449

4550
### Date
4651

52+
- Equal: `$date == "2006-01-02T15:04:05Z07:00"`
53+
- Before (strict): `$date < "2006-01-02T15:04:05Z07:00"`
54+
- Before: `$date <= "2006-01-02T15:04:05Z07:00"`
55+
- After (strict): `$date > "2006-01-02T15:04:05Z07:00"`
4756
- Before: `$date <= "2006-01-02T15:04:05Z07:00"`
48-
- After: `$date >= "2006-01-02T15:04:05Z07:00"`
49-
50-
### Symbols
51-
52-
- In:`$sym in [#a, #b, #c]`
53-
- Not in:`$sym not in [#a, #b, #c]`
5457

5558
### Bytes
5659

5760
- Equal: `$b == "hex:3df97fb5"`
58-
- In: `$b in ["hex:3df97fb5", "hex:4a8feed1"]`
59-
- Not in: `$b not in ["hex:3df97fb5", "hex:4a8feed1"]`
61+
- Length: `$b.length()`
6062

6163
### Set
6264

63-
- Any: `$set in [#read, #write]`
64-
- None: `$set not in [#read, #write]`
65+
- Equal: `$set == ["a", "b"]`
66+
- Contains (element membership): `$set.contains("a")`
67+
- Contains (set inclusion): `$set.contains([a])`
68+
- Union: `$set.union(["a"])`
69+
- Intersection: `$set.intersection(["a"])`
70+
- Length: `$set.length()`
71+
72+
### Operators precedence
73+
74+
The operators have the following precedence (highest to lowest):
75+
76+
77+
| Operators | Associativity |
78+
|-----------------------------|------------------|
79+
| `!` (prefix) | not associative |
80+
| `*`, `/` | left-associative |
81+
| `+`, `-` | left-associative |
82+
| `>`, `>=`, `<`, `<=`, `==` | not associative |
83+
| `&&` | left-associative |
84+
| `||` | left-associative |
85+
86+
Parentheses can be used to force precedence (or to make it explicit).
87+
6588

6689
## Fact
6790

68-
A fact is a single predicate that does not contain any variables, e.g. `right(#authority, "file1.txt", #read)`.
91+
A fact is a single predicate that does not contain any variables, e.g. `right("file1.txt", "read")`.
6992

7093
# Rule
7194

7295
A rule is formed from a head, a body, and a list of constraints.
73-
The head is a single predicate, the body is a list of predicates, and followed by an optional list of constraints.
96+
The head is a single predicate, the body is a list of predicates or constraints. Variables present in the head and in constraints must be introduced by predicates in the body.
7497

75-
It has the format: `Head <- Body @ Constraints`.
98+
It has the format: `Head <- (predicate, constraint)+`.
7699

77-
e.g. `right(#authority, $file, #read) <- resource(#ambient, $file), owner(#ambient, $user, $file) @ $user == "username", prefix($file, "/home/username")`
100+
e.g. `right($file, "read") <- resource($file), owner($user, $file), $user == "username", $file.starts_with("/home/username")`
78101

79102
# Check
80103

81-
A check is a list of rules with the format: `[ rule0 || rule1 || ... || ruleN ]`
104+
A check starts with `check if`, followed by one or more rule bodies, separated with ` or `.
105+
106+
# Policy
107+
108+
A policy starts with either `allow if` or `deny if`, followed by one or more rule bodies, separated with ` or `.

0 commit comments

Comments
 (0)