diff --git a/ast/ast_test.go b/ast/ast_test.go index 92888e7..e2b249d 100644 --- a/ast/ast_test.go +++ b/ast/ast_test.go @@ -11,74 +11,6 @@ import ( "github.com/cedar-policy/cedar-go/types" ) -// This example shows how you can construct polcies with the ast package -func Example() { - - johnny := types.NewEntityUID("User", "johnny") - sow := types.NewEntityUID("Action", "sow") - cast := types.NewEntityUID("Action", "cast") - - // @example("one") - // permit ( - // principal == User::"johnny" - // action in [Action::"sow", Action::"cast"] - // resource - // ) - // when { true } - // unless { false }; - _ = ast.Annotation("example", "one"). - Permit(). - PrincipalIsIn("User", johnny). - ActionInSet(sow, cast). - When(ast.True()). - Unless(ast.False()) - - // @example("two") - // forbid (principal, action, resource) - // when { resource.tags.contains("private") } - // unless { resource in principal.allowed_resources }; - private := "private" - _ = ast.Annotation("example", "two"). - Forbid(). - When( - ast.Resource().Access("tags").Contains(ast.String(private)), - ). - Unless( - ast.Resource().In(ast.Principal().Access("allowed_resources")), - ) - - // forbid (principal, action, resource) - // when { {x: "value"}.x == "value" } - // when { {x: 1 + context.fooCount}.x == 3 } - // when { [1, (2 + 3) * 4, context.fooCount].contains(1) }; - simpleRecord := types.NewRecord(types.RecordMap{ - "x": types.String("value"), - }) - _ = ast.Forbid(). - When( - ast.Value(simpleRecord).Access("x").Equal(ast.String("value")), - ). - When( - ast.Record(ast.Pairs{{Key: "x", Value: ast.Long(1).Add(ast.Context().Access("fooCount"))}}).Access("x").Equal(ast.Long(3)), - ). - When( - ast.Set( - ast.Long(1), - ast.Long(2).Add(ast.Long(3)).Multiply(ast.Long(4)), - ast.Context().Access("fooCount"), - ).Contains(ast.Long(1)), - ) - - // forbid (principal, action, resource) - // when { resource.angleRadians.greaterThan(decimal("3.1415")) } - _ = ast.Forbid(). - When( - ast.Resource().Access("angleRadians").DecimalGreaterThan( - ast.DecimalExtensionCall(ast.String("3.1415")), - ), - ) -} - func TestASTByTable(t *testing.T) { t.Parallel() tests := []struct { diff --git a/ast/example_test.go b/ast/example_test.go new file mode 100644 index 0000000..d681d62 --- /dev/null +++ b/ast/example_test.go @@ -0,0 +1,114 @@ +package ast_test + +import ( + "fmt" + + "github.com/cedar-policy/cedar-go/ast" + "github.com/cedar-policy/cedar-go/types" +) + +// This example shows a basic programmatic AST construction via the Permit() builder: +func Example() { + johnny := types.NewEntityUID("FolkHeroes", "johnnyChapman") + sow := types.NewEntityUID("Action", "sow") + cast := types.NewEntityUID("Action", "cast") + midwest := types.NewEntityUID("Locations::USA::Regions", "midwest") + + policy := ast.Permit(). + PrincipalEq(johnny). + ActionInSet(sow, cast). + ResourceIs("Crops::Apple"). + When(ast.Context().Access("location").In(ast.Value(midwest))). + Unless(ast.Context().Access("season").Equal(ast.String("winter"))) + + fmt.Println(string(policy.MarshalCedar())) + + // Output: + // permit ( + // principal == FolkHeroes::"johnnyChapman", + // action in [Action::"sow", Action::"cast"], + // resource is Crops::Apple + // ) + // when { context.location in Locations::USA::Regions::"midwest" } + // unless { context.season == "winter" }; +} + +// To programmatically create policies with annotations, use the Annotation() builder: +func Example_annotation() { + policy := ast.Annotation("example1", "value"). + Annotation("example2", ""). + Forbid() + + fmt.Println(string(policy.MarshalCedar())) + + // Output: + // @example1("value") + // @example2("") + // forbid ( principal, action, resource ); +} + +// This example shows how precedence can be expressed using the AST builder syntax: +func Example_precedence() { + // The argument passed to .Add() is the entire right-hand side of the expression, so 1 + 5 is evaluated with + // higher precedence than the subsequent multiplication by 10. + policy := ast.Permit(). + When(ast.Long(1).Add(ast.Long(5)).Multiply(ast.Long(10)).Equal(ast.Long(60))) + + fmt.Println(string(policy.MarshalCedar())) + + // Output: + // permit ( principal, action, resource ) + // when { (1 + 5) * 10 == 60 }; +} + +// Extension functions can be explicitly called by using the appropriate builder with the ExtensionCall suffix. This +// example demonstrates the use of DecimalExtensionCall(): +func Example_explicitExtensionCall() { + policy := ast.Forbid(). + When( + ast.Resource().Access("angleRadians").DecimalGreaterThan( + ast.DecimalExtensionCall(ast.String("3.1415")), + ), + ) + + fmt.Println(string(policy.MarshalCedar())) + + // Output: + // forbid ( principal, action, resource ) + // when { resource.angleRadians.greaterThan(decimal("3.1415")) }; +} + +func ExampleRecord() { + // Literal records can be constructed and passed via the ast.Value() builder + literalRecord := types.NewRecord(types.RecordMap{ + "x": types.String("value1"), + "y": types.String("value2"), + }) + + // Records with internal expressions are constructed via the ast.Record() builder + exprRecord := ast.Record(ast.Pairs{ + { + Key: "x", + Value: ast.Long(1).Add(ast.Context().Access("fooCount")), + }, + { + Key: "y", + Value: ast.Long(8), + }, + }) + + policy := ast.Forbid(). + When( + ast.Value(literalRecord).Access("x").Equal(ast.String("value1")), + ). + When( + exprRecord.Access("x").Equal(ast.Long(3)), + ) + + fmt.Println(string(policy.MarshalCedar())) + + // Output: + // forbid ( principal, action, resource ) + // when { {"x":"value1", "y":"value2"}.x == "value1" } + // when { {"x":(1 + context.fooCount), "y":8}.x == 3 }; +} diff --git a/ast/policy.go b/ast/policy.go index 95bcf69..be2ddc9 100644 --- a/ast/policy.go +++ b/ast/policy.go @@ -1,6 +1,18 @@ +// Package ast provides functions for programmatically constructing a Cedar policy AST. +// +// Programmatically generated policies are germinated by calling one of the following top-level functions: +// - [Permit] +// - [Forbid] +// - [Annotation] package ast -import "github.com/cedar-policy/cedar-go/internal/ast" +import ( + "bytes" + + "github.com/cedar-policy/cedar-go/internal/ast" + "github.com/cedar-policy/cedar-go/internal/json" + "github.com/cedar-policy/cedar-go/internal/parser" +) type Policy ast.Policy @@ -31,3 +43,48 @@ func (p *Policy) When(node Node) *Policy { func (p *Policy) Unless(node Node) *Policy { return wrapPolicy(p.unwrap().Unless(node.Node)) } + +// MarshalJSON encodes a single Policy statement in the JSON format specified by the [Cedar documentation]. +// +// [Cedar documentation]: https://docs.cedarpolicy.com/policies/json-format.html +func (p *Policy) MarshalJSON() ([]byte, error) { + jsonPolicy := (*json.Policy)(p) + return jsonPolicy.MarshalJSON() +} + +// UnmarshalJSON parses and compiles a single Policy statement in the JSON format specified by the [Cedar documentation]. +// +// [Cedar documentation]: https://docs.cedarpolicy.com/policies/json-format.html +func (p *Policy) UnmarshalJSON(b []byte) error { + var jsonPolicy json.Policy + if err := jsonPolicy.UnmarshalJSON(b); err != nil { + return err + } + + *p = (Policy)(jsonPolicy) + return nil +} + +// MarshalCedar encodes a single Policy statement in the human-readable format specified by the [Cedar documentation]. +// +// [Cedar documentation]: https://docs.cedarpolicy.com/policies/syntax-grammar.html +func (p *Policy) MarshalCedar() []byte { + cedarPolicy := (*parser.Policy)(p) + + var buf bytes.Buffer + cedarPolicy.MarshalCedar(&buf) + + return buf.Bytes() +} + +// UnmarshalCedar parses and compiles a single Policy statement in the human-readable format specified by the [Cedar documentation]. +// +// [Cedar documentation]: https://docs.cedarpolicy.com/policies/syntax-grammar.html +func (p *Policy) UnmarshalCedar(b []byte) error { + var cedarPolicy parser.Policy + if err := cedarPolicy.UnmarshalCedar(b); err != nil { + return err + } + *p = (Policy)(cedarPolicy) + return nil +} diff --git a/ast/policy_test.go b/ast/policy_test.go new file mode 100644 index 0000000..7e817ec --- /dev/null +++ b/ast/policy_test.go @@ -0,0 +1,58 @@ +package ast_test + +import ( + "testing" + + "github.com/cedar-policy/cedar-go/ast" + internalast "github.com/cedar-policy/cedar-go/internal/ast" + "github.com/cedar-policy/cedar-go/internal/testutil" + "github.com/cedar-policy/cedar-go/types" +) + +func TestPolicy_MarshalJSON(t *testing.T) { + t.Parallel() + + p := ast.Permit().PrincipalEq(types.NewEntityUID("Foo::Bar", "Baz")) + expected := `{ + "effect": "permit", + "principal": { + "op": "==", + "entity": { + "type": "Foo::Bar", + "id": "Baz" + } + }, + "action": { + "op": "All" + }, + "resource": { + "op": "All" + } + }` + testutil.JSONMarshalsTo(t, p, expected) + + var unmarshaled ast.Policy + err := unmarshaled.UnmarshalJSON([]byte(expected)) + testutil.OK(t, err) + testutil.Equals(t, &unmarshaled, p) +} + +func TestPolicy_MarshalCedar(t *testing.T) { + t.Parallel() + + p := ast.Permit().PrincipalEq(types.NewEntityUID("Foo::Bar", "Baz")) + expected := `permit ( + principal == Foo::Bar::"Baz", + action, + resource +);` + + testutil.Equals(t, string(p.MarshalCedar()), expected) + + var unmarshaled ast.Policy + err := unmarshaled.UnmarshalCedar([]byte(expected)) + + p.Position = internalast.Position{Offset: 0, Line: 1, Column: 1} + testutil.OK(t, err) + testutil.Equals(t, &unmarshaled, p) +} diff --git a/policy.go b/policy.go index d2f4c6d..b9fc039 100644 --- a/policy.go +++ b/policy.go @@ -42,6 +42,9 @@ func (p *Policy) UnmarshalJSON(b []byte) error { return nil } +// MarshalCedar encodes a single Policy statement in the human-readable format specified by the [Cedar documentation]. +// +// [Cedar documentation]: https://docs.cedarpolicy.com/policies/syntax-grammar.html func (p *Policy) MarshalCedar() []byte { cedarPolicy := (*parser.Policy)(p.ast) @@ -51,6 +54,9 @@ func (p *Policy) MarshalCedar() []byte { return buf.Bytes() } +// UnmarshalCedar parses and compiles a single Policy statement in the human-readable format specified by the [Cedar documentation]. +// +// [Cedar documentation]: https://docs.cedarpolicy.com/policies/syntax-grammar.html func (p *Policy) UnmarshalCedar(b []byte) error { var cedarPolicy parser.Policy if err := cedarPolicy.UnmarshalCedar(b); err != nil {