Skip to content

Commit

Permalink
Merge pull request cedar-policy#63 from strongdm/add-ast-docs
Browse files Browse the repository at this point in the history
ast: restructure ast package documentation
  • Loading branch information
patjakdev authored Nov 12, 2024
2 parents 2bc36c6 + d1f6529 commit 96c854e
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 69 deletions.
68 changes: 0 additions & 68 deletions ast/ast_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
114 changes: 114 additions & 0 deletions ast/example_test.go
Original file line number Diff line number Diff line change
@@ -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 };
}
59 changes: 58 additions & 1 deletion ast/policy.go
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
}
58 changes: 58 additions & 0 deletions ast/policy_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
6 changes: 6 additions & 0 deletions policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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 {
Expand Down

0 comments on commit 96c854e

Please sign in to comment.