diff --git a/README.md b/README.md index fa9b621..fb02294 100644 --- a/README.md +++ b/README.md @@ -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 { @@ -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 { @@ -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 @@ -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) @@ -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 diff --git a/parser/GRAMMAR.md b/parser/GRAMMAR.md index 7afecbb..4097819 100644 --- a/parser/GRAMMAR.md +++ b/parser/GRAMMAR.md @@ -4,25 +4,31 @@ 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` @@ -30,52 +36,73 @@ Constraints allows performing checks on a variable, below is the list of availab - 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 `.