diff --git a/README.md b/README.md index 3ed8641..569968e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# UCAN Delegation Specification v1.0.0-rc.1 +# UCAN Delegation Specification +## Version 1.0.0-rc.1 ## Editors @@ -113,7 +114,7 @@ In the case where access to an [external resource] is delegated, the Subject MUS "sub": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", "cmd": "/crud/create", "pol": [ - ["==", ".uri", "https://example.com/blog/"], // Resource + ["==", ".url", "https://example.com/blog/"], // Resource managed by the Subject ["==", ".status", "draft"] ], // ... @@ -122,17 +123,19 @@ In the case where access to an [external resource] is delegated, the Subject MUS # Policy Language -UCAN Delegation uses a minimal predicate language as a policy language. Policies are syntactically driven, and MUST constrain the `args` field of an eventual [Invocation]. +UCAN Delegation uses predicate logic statements extended with [jq]-style selectors as a policy language. Policies are syntactically driven, and MUST constrain the `args` field of an eventual [Invocation]. +A Policy is always given as an array of predicates. This top-level array is implicitly treated as a logical `and`, where `args` MUST pass validation of every top-level predicate. -The grammar of the UCAN Policy Language is as follows: +Policies are structured as trees. With the exception of subtrees under `some`, `or`, and `not`, every leaf MUST evaluate to `true`. `some`, `or`, and `not` must -## Syntax +A Policy is an array of statements. Every statement MUST take the form `[operator, selector, argument]` except for nergation which MUST take the form `["not", term]`. +Below is a formal syntax for the UCAN Policy Language given in [ABNF] (for DAG-JSON): ``` abnf -policy = "[" *predicate "]" -predicate = equality +policy = "[" *1(statement *(", " statement)) "]" +statement = equality / inequality / match / connective @@ -140,15 +143,16 @@ predicate = equality ;; STRUCTURAL -connective = "['not', " policy "]" ; Negation - / "['and', " policy "]" ; Conjuction - / "['or', " policy "]" ; Disjunction +connective = "['not', " statement "]" ; Negation + / "['and', [" statement *(", " statement) "]]" ; Conjuction + / "['or', [" statement *(", " statement) "]]" ; Disjunction quanitifier = "['every', " selector ", " policy "]" ; Universal / "['some', " selector ", " policy "]" ; Existential ;; COMPARISONS +// FIXME DQUOTE equality = "['==', " selector ", " ipld "]" ; Equality on IPLD literals inequality = "['>', " selector ", " number "]" ; Numeric greater-than / "['>=', " selector ", " number "]" ; Numeric greater-than-or-equal @@ -171,14 +175,123 @@ subselector = "." 1*utf8 ; Dotted field selector pattern = *utf8 number = integer / float ``` + +### Comparisons -### Pattern +| Operator | Argument(s) | Example | +|----------|-------------------------------|----------------------------------| +| `==` | `Selector, IPLD` | `["==", ".a", [1, 2, {"b": 3}]]` | +| `<` | `Selector, (float | integer)` | `["<", ".a", 1]` | +| `<=` | `Selector, (float | integer)` | `["<=", ".a", 1]` | +| `>` | `Selector, (float | integer)` | `[">", ".a", 1]` | +| `>=` | `Selector, (float | integer)` | `[">=", ".a", 1]` | -## Semantics +Literal equality (`==`) MUST match the resolved selecor to entire IPLD argument. This is a "deep comparison". -A Policy is always given as an array of predicates. This top-level array is implicitly treated as a logical `and`, where `args` MUST pass validation of every top-level predicate. +Numeric inequalities MUST be agnostic to numeric type. In other words, the decimal representation is considered equivalent to an integer (`1 == 1.0 == 1.00`). Attempting to compare on a non-number MUST return false and MUST NOT throw an exception. -Policies are structured as trees. With the exception of subtrees under `some`, `or`, and `not`, every leaf MUST evaluate to `true`. `some`, `or`, and `not` must +### Glob Matching + +| Operator | Argument(s) | Example | +|----------|---------------------|---------------------------------------| +| `match` | `Selector, Pattern` | `["==", ".email", "*@*.example.com"]` | + +Glob patterns MUST only include a specicial character: `*` ("wildcard"). There is no single character matcher. `*` literals MUST be escaped (`"\*"`). Attempting to match on a non-string MUST return false and MUST NOT throw an exception. + +The wildcard represents zero-or-more characters. The following string literals MUST pass validation for the pattern `"Alice\*, Bob*, Carol.`: + +* `"Alice*, Bob, Carol."` +* `"Alice*, Bob, Dan, Erin, Carol."` +* `"Alice*, Bob*, Carol."` + +The following MUST NOT pass validation for that same pattern: + +* `"Alice*, Bob, Carol"` (missing the final `.`) +* `"Alice*, Bob*, Carol!"` (final `.` MUST NOT be treated as a wildcard) +* `"Alice, Bob, Carol."` (missing the `*` after `Alice`) +* `"Alice Cooper, Bob, Carol."` (the `*` after `Alice` is an escaped literal in the pattern) +* `" Alice*, Bob, Carol. "` (whitespace in the pattern is significant) + +### Connectives + +Connectives add context to their enclosed statement(s). + +| Operator | Argument(s) | Example | +|----------|---------------|--------------------------------------------| +| `not` | `Statement` | `["not", [">", ".a", 1]]` | +| `and` | `[Statement]` | `["and", [[">", ".a", 1], [">", ".b", 2]]` | +| `or` | `[Statement]` | `["or", [[">", ".a", 1], [">", ".b", 2]]` | + +`not` MUST invert the truth value of the inner statement. For example, if `["==", ".a", 1]` were false (`.a` is not 1), then `["not", ["==", ".a", 1]]` would be true. + +`and` MUST take an arbitrarily long array of statements, and require that every inner statement be true. An empty array MUST be treated as vacuously true. + +`or` MUST take an arbitrarily long array of statements, and require that at least one inner statement be true. An empty array MUST be treated as vacuously true. + +### Quantification + +When a selector resolves to a collection (an array or map), quantifiers provide a way to extend `and` and `or` to their contents. Attempting to quantify over a non-collection MUST return false and MUST NOT throw an exception. + +Quantifying over an array is straightforward: it MUST apply the inner statement to each array value. Quantifying over a map MUST extract the values (discarding the keys), and then MUST proceed onthe values the same as if it were an array. + +| Operator | Argument(s) | Example | +|----------|-------------------------|----------------------------------| +| `every` | `Selector, [Statement]` | `["every", ".a" [">", ".b", 1]]` | +| `some` | `Selector, [Statement]` | `["some", ".a" [">", ".b", 1]]` | + +`every` extends `and` over collections. `some` extends `or` over collections. For example: + +``` js +const args = {"a": [{"b": 1}, {"b": 2}, {"z": [7, 8, 9]}]} +const statement = ["every", ".a", [">", ".b", 0]] + +// Reduction +["every", [{"b": 1}, {"b": 2}, {"z": [7, 8, 9]}], [">", ".b", 0]] + +["and", [ + [ + [">", 1, 0], + [">", 2, 0], + [">", null, 0] + ] +] + +["and", + [ + true, + true, + false // <- + ] +] + +false // ❌ +``` + +``` js +const args = {"a": [{"b": 1}, {"b": 2}, {"z": [7, 8, 9]}]} +const statement = ["some", ".a", ["==", ".b", 2]] + +// Reduction +["some", [{"b": 1}, {"b": 2}, {"z": [7, 8, 9]}], ["==", ".b", 2]] + +["or", + [ + ["==", 1, 2], + ["==", 2, 2], + ["==", null, 2] + ] +] + +["or", + [ + false, + true, + false + ] +] + +true // ✅ +``` ### Selectors @@ -287,21 +400,40 @@ null +### Selector Taxonomy + +jq is a much larger language, and includes things like pipes, arithmatic, regexes, assignment, recursive descent, and so on. The UCAN policy language MUST only include the following features: + +| Selector Name | Examples | Notes | +|---------------------------|----------------------------|-------------------------------------------------------------------------------------------------| +| Identity | `.` | Take the entire argument | +| Dotted field name | `.foo`, `.bar0_` | Shorthand for selecting in a map by key (with exceptions, see below) | +| Unambiguous field name | `["."]` | Select in a map by arbitrary key | +| Collection values | `[]` | Expands out all of the children that match the remaining path FIXME double check with the code | +| Collection index | `[0]`, `[42]` | The element at an index. On a map, this is decided by IPLD's key sort order. FIXME double check | +| Collection negative index | `[-1]`, `[-42]` | The element by index from the end. `-1` is the index for the last element. | +| Try | `.foo?`, `.?`, `["nope"]?` | Returns `null` on what would otherwise fail | + +Any selection MAY begin and/or end with a single dot. Multiple dots (e.g. `..`, `...`) MUST NOT be used anywhere in a selector. + +The try operator is idempotent, and repeated tries (`.foo???`) MUST be allowed. + ### Validation -Validation +Validation involves substituting the values from the `args` field into the Policy, and evaluating the predicate. Since Policies are tree structured, selector substitution and predicate evaluation MAY proceed in any order. + +If a selector cannot be resolved (there is no value at that path), -Being tree structured, evaluation MAY proceed in any order. Here is one step-by-step example: +#### Example Evaluation -
-Example Context +Below is a step-by-step evaluation example: ``` js { // Invocation "cmd": "/msg/send", "args": { "from": "alice@example.com", - "to": ["bob@example.com", "carol@elsewhere.example.com"], + "to": ["bob@example.com", "carol@not.example.com"], "title": "Coffee", "body": "Still on for coffee" }, @@ -318,130 +450,47 @@ Being tree structured, evaluation MAY proceed in any order. Here is one step-by- } ``` -
- -
-Code Example - ``` js -// Extract policy -[ +[ // Extract policy ["==", ".from", "alice@example.com"], ["some", ".to", ["match", ".", "*@example.com"]] ] -// Resolve selectors -[ +[ // Resolve selectors ["==", "alice@example.com", "alice@example.com"], ["some", ["bob@example.com", "carol@elsewhere.example.com"], ["match", ".", "*@example.com"]] ] -// Expand quantifier -[ +[ // Expand quantifier ["==", "alice@example.com", "alice@example.com"], ["or", [ ["match", "bob@example.com", "*@example.com"] - ["match", "carol@elsewhere.example.com", "*@example.com"]]] + ["match", "carol@elsewhere.example.com", "*@example.com"]] + ] ] -// Evaluate first clause -[ +[ // Evaluate first predicate true, ["or", [ ["match", "bob@example.com", "*@example.com"] ["match", "carol@elsewhere.example.com", "*@example.com"]]] ] -// Evaluate second clause's children -[ +[ // Evaluate second predicate's children true, ["or", [true, false]] ] -// Evaluate second clause -[true, true] -// Implicit top-level `and` -true -``` - -
- -
- -Graphical Example - -``` mermaid -flowchart BT - policy - eq["=="] --> policy - from[".from"] --> eq - alice["alice@example.com"] --> eq - - some --> policy - to[".to"] --> some - match --> some - this["."] --> match - pattern["*@example.com"] --> match -``` - -``` mermaid -flowchart BT - policy - eq["=="] --> policy - from["alice@example.com"] --> eq - alice["alice@example.com"] --> eq - - some --> policy - to["[]"] --> some - bob["bob@example.com"] --> to - carol["carol@not.example.com"] --> to - match --> some - this["."] --> match - pattern["*@example.com"] --> match -``` - -``` mermaid -flowchart BT - policy - eq["=="] --> policy - from["alice@example.com"] --> eq - alice["alice@example.com"] --> eq - - or --> policy - match_bob --> or - bob["bob@example.com"] --> match_bob - bob_pattern["*@example.com"] --> match_bob - - match_carol --> or - carol["carol@not.example.com"] --> match_carol - carol_pattern["*@example.com"] --> match_carol -``` - -``` mermaid -flowchart BT - policy - alice["true"] --> policy - - or --> policy - match_bob["true"] --> or - match_carol["false"] --> or -``` - -``` mermaid -flowchart BT - policy - alice["true"] --> policy - or["true"] --> policy -``` +[ // Evaluate second predicate + true, + true +] -``` mermaid -flowchart BT - true +// Evaluate top-level `and` +true ``` -
- Any arguments MUST be taken verbatim and MUST NOT be further adjusted. For more flexible validation of Arguments, use [Conditions]. Note that this also applies to arrays and objects. For example, the `to` array in this example is considered to be exact, so the Invocation fails validation in this case: @@ -449,7 +498,7 @@ Note that this also applies to arrays and objects. For example, the `to` array i ``` js // Delegation { - "cmd": "/msg/send", + "cmd": "/email/send", "pol": [ ["==", ".from", "alice@example.com"], ["some", ".to", ["match", ".", "*@example.com"]] @@ -459,7 +508,7 @@ Note that this also applies to arrays and objects. For example, the `to` array i // VALID Invocation { - "cmd": "/msg/send", + "cmd": "/email/send", "args": { "from": "alice@example.com", "to": ["bob@example.com", "carol@elsewhere.example.com"], @@ -471,10 +520,10 @@ Note that this also applies to arrays and objects. For example, the `to` array i // INVALID Invocation { - "cmd": "/msg/send", + "cmd": "/email/send", "args": { "from": "alice@example.com", - "to": ["carol@elsewhere.example.com"], // Missing `*@example.com` + "to": ["carol@elsewhere.example.com"], // No match for `*@example.com` "title": "Coffee", "body": "Still on for coffee" }, @@ -527,56 +576,9 @@ Condition semantics MUST be established by the Subject. They are openly extensib The above Delegation MUST be interpreted as "may send email from `alice@example.com` on Mondays as long as `bob@exmaple.com` is among the recipients" -The `if` field MUST take the following shape: `[{}]`. The array represents a logical `all` (chained `AND`s). To represent logical `OR`, issue another delegation with that attenuation. For instance, the following represents `{a: 1, b:2} AND {c: 3} AND {d: 4}`: - -``` js -[ - { // ┐ - a: 1, // ├─ Confition ─┐ - b: 2 // │ │ - }, // ┘ ├─ AND ─┐ - { // ┐ │ │ - c: 3 // ├─ Condition ─┘ │ - }, // ┘ ├─ AND - { // ┐ │ - d: 4 // ├─ Condition ─────────┘ - } // ┘ -] -``` - -Expressing Conditions in this standard way simplifies ad hoc extension at delegation time. As a concrete example, below a Condition is added to the constraints on the `to` field. - -``` js -// Original Condition -[ - { - "field": "to", - "includes": "bob@example.com" - } -] - -// Attenuated Conditions -[ - { // Same as above - "field": "to", - "includes": "bob@example.com" - }, - { // Also must send to carol@example.com - "field": "to", - "includes": "carol@example.com" - }, - { // Only send to @example.com addresses - "field": "to", - "elements": { - "match": "/.+@example.com/" - } - } -] -``` - # Validation -Validation of a UCAN chain MAY occur at any time, but MUST occur upon receipt of an [Invocation] prior to execution. While proof chains exist outside of a particular delegation (and are made concrete in [UCAN Invocation]s), each delegate MUST store one or more valid delegations chains for a particular claim. +Validation of a UCAN chain MAY occur at any time, but MUST occur upon receipt of an [Invocation] _prior to execution_. While proof chains exist outside of a particular delegation (and are made concrete in [UCAN Invocation]s), each delegate MUST store one or more valid delegations chains for a particular claim. Each capability has its own semantics, which needs to be interpretable by the [Executor]. Therefore, a validator MUST NOT reject all capabilities when one that is not relevant to them is not understood. For example, if a Condition fails a delegation check at execution time, but is not relevant to the invocation, it MUST be ignored.