Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ast: restructure ast package documentation #63

Merged
merged 1 commit into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading