Skip to content

Commit

Permalink
Update readme & grammar documentation (#125)
Browse files Browse the repository at this point in the history
* doc: update readme with up-to-date code examples

* doc: update grammar reference

It referenced biscuit v0 / biscuit v1 syntax
  • Loading branch information
divarvel authored Jun 26, 2023
1 parent 53ef352 commit 1514f11
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 53 deletions.
79 changes: 53 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,38 @@ biscuit-go is an implementation of [Biscuit](https://github.com/biscuit-auth/bis
```go
rng := rand.Reader
publicRoot, privateRoot, _ := ed25519.GenerateKey(rng)
builder := biscuit.NewBuilder(privateRoot)

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

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

// ... add more authority facts, rules, caveats...
builder := biscuit.NewBuilder(privateRoot)
builder.AddBlock(authority)

b, err := builder.Build()
if err != nil {
panic(fmt.Errorf("failed to build biscuit: %v", err))
panic(fmt.Errorf("failed to build biscuit: %v", err))
}

token, err := b.Serialize()
if err != nil {
panic(fmt.Errorf("failed to serialize biscuit: %v", err))
panic(fmt.Errorf("failed to serialize biscuit: %v", err))
}

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

#### Attenuate a biscuit

```go
b, err = biscuit.Unmarshal(token)
if err != nil {
Expand All @@ -52,9 +55,13 @@ if err != nil {

// Attenuate the biscuit by appending a new block to it
blockBuilder := b.CreateBlock()
blockBuilder.AddFact(biscuit.Fact{/* ... */})

// ... add more facts, rules, caveats...
block, err := parser.FromStringBlockWithParams(`
check if resource($file), operation($permission), [{read}].contains($permission);`,
map[string]biscuit.Term{"read": biscuit.String("read")})
if err != nil {
panic(fmt.Errorf("failed to parse block: %v", err))
}
blockBuilder.AddBlock(block)

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

// token is now a []byte attenuation of the original token, and ready to be shared
// attenuatedToken is a []byte, representing an attenuated token
```

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

fact1, err := parser.FromStringFact(`resource("/a/file1.txt")`)
authorizerContents, err := parser.FromStringAuthorizerWithParams(`
resource({res});
operation({op});
allow if right({res}, {op});
`, map[string]biscuit.Term{"res": biscuit.String("/a/file1.txt"), "op": biscuit.String("read")})
if err != nil {
panic(fmt.Errorf("failed to parse authority facts: %v", err))
panic(fmt.Errorf("failed to parse authorizer: %v", err))
}

auhorizer.AddFact(fact1)

// ... add more ambient facts, rules, caveats...

authorizer.AddPolicy(biscuit.DefaultAllowPolicy)
authorizer.AddAuthorizer(authorizerContents)

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

### Using biscuit-go grammar

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.
biscuit-go provides a datalog parser, allowing to input datalog elements as plain strings, along with support for parameter substitution.

See [GRAMMAR reference](./parser/GRAMMAR.md) for the complete syntax.

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.

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`).

#### Panic on parsing errors

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:

```go
p := parser.New()
b.AddFact(p.Must().Fact(`resource("/a/file1.txt")`))
b := biscuit.NewBuilder(privateRoot)

b.AddBlock(p.Must().Block(`
right("/a/file1.txt", {read});
right("/a/file1.txt", {write});
right("/a/file2.txt", {read});
right("/a/file3.txt", {write});
`, map[string]biscuit.Term{"read": biscuit.String("read"), "write": biscuit.String("write")}))

b.AddFact(p.Must().Fact(`resource({res})`, map[string]biscuit.Term{"res": biscuit.String("/a/file1.txt")}))
b.AddRule(p.Must().Rule(`
can_read($file)
<- resource($file)
$file.starts_with("/a/")
`))
`, nil))
```

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.

## Examples

- [example_test.go](./example_test.go) for a simple use case
Expand Down
81 changes: 54 additions & 27 deletions parser/GRAMMAR.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,78 +4,105 @@ This document describes the currently supported Datalog grammar.

## Term

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

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

## Predicate

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

## Constraints

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

### Boolean

- Equal: `$b == true`
- Negation: `!$b`
- And / Or: `$b || $c && $d`

### Integer

- Equal: `$i == 1`
- Greater than: `$i > 1`
- Greater than or equal: `$i >= 1`
- Less than: `$i < 1`
- Less than or equal: `$i <= 1`
- In: `$i in [1, 2, 3]`
- Not in: `$i not in [1, 2, 3]`
- Arithmetic (`*`, `/`, `+`, `-`)

### String

- Equal: `$s == "abc"`
- Starts with: `prefix($s, "abc")`
- Ends with: `suffix($s, "abc")`
- Regular expression: `match($s, "^abc\s+def$") `
- In: `$s in ["abc", "def"]`
- Not in: `$s not in ["abc", "def"]`
- Starts with: `$s.starts_with("abc")`
- Ends with: `$s.ends_with("abc")`
- Regular expression: `$s.matches("^abc\s+def$") `
- Contains: `$s.contains("abc")`
- Length: `$s.length()`

### Date

- Equal: `$date == "2006-01-02T15:04:05Z07:00"`
- Before (strict): `$date < "2006-01-02T15:04:05Z07:00"`
- Before: `$date <= "2006-01-02T15:04:05Z07:00"`
- After (strict): `$date > "2006-01-02T15:04:05Z07:00"`
- Before: `$date <= "2006-01-02T15:04:05Z07:00"`
- After: `$date >= "2006-01-02T15:04:05Z07:00"`

### Symbols

- In:`$sym in [#a, #b, #c]`
- Not in:`$sym not in [#a, #b, #c]`

### Bytes

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

### Set

- Any: `$set in [#read, #write]`
- None: `$set not in [#read, #write]`
- Equal: `$set == ["a", "b"]`
- Contains (element membership): `$set.contains("a")`
- Contains (set inclusion): `$set.contains([a])`
- Union: `$set.union(["a"])`
- Intersection: `$set.intersection(["a"])`
- Length: `$set.length()`

### Operators precedence

The operators have the following precedence (highest to lowest):


| Operators | Associativity |
|-----------------------------|------------------|
| `!` (prefix) | not associative |
| `*`, `/` | left-associative |
| `+`, `-` | left-associative |
| `>`, `>=`, `<`, `<=`, `==` | not associative |
| `&&` | left-associative |
| `||` | left-associative |

Parentheses can be used to force precedence (or to make it explicit).


## Fact

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

# Rule

A rule is formed from a head, a body, and a list of constraints.
The head is a single predicate, the body is a list of predicates, and followed by an optional list of constraints.
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.

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

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

# Check

A check is a list of rules with the format: `[ rule0 || rule1 || ... || ruleN ]`
A check starts with `check if`, followed by one or more rule bodies, separated with ` or `.

# Policy

A policy starts with either `allow if` or `deny if`, followed by one or more rule bodies, separated with ` or `.

0 comments on commit 1514f11

Please sign in to comment.