Skip to content

Commit

Permalink
Merge pull request cedar-policy#34 from strongdm/idx-46/performance
Browse files Browse the repository at this point in the history
General performance improvements and experimental batch mode
  • Loading branch information
patjakdev authored Sep 13, 2024
2 parents 6f1b20e + 39329b6 commit efe5690
Show file tree
Hide file tree
Showing 23 changed files with 4,702 additions and 485 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.idea/
tmp/
tmp/
.DS_Store
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ While in development (0.x.y), each tagged release may contain breaking changes.

## Change log

### New features in 0.4.x

- General performance improvements to the evaluator
- An experimental batch evaluator has been added to `x/exp/batch`

### New features in 0.2.x

- A programmatic AST is now available in the `ast` package.
Expand Down
103 changes: 24 additions & 79 deletions authorize.go
Original file line number Diff line number Diff line change
@@ -1,116 +1,61 @@
package cedar

import (
"fmt"

"github.com/cedar-policy/cedar-go/internal/eval"
"github.com/cedar-policy/cedar-go/types"
)

// A Decision is the result of the authorization.
type Decision bool
type Request = types.Request
type Decision = types.Decision
type Diagnostic = types.Diagnostic
type DiagnosticReason = types.DiagnosticReason
type DiagnosticError = types.DiagnosticError

// Each authorization results in one of these Decisions.
const (
Allow = Decision(true)
Deny = Decision(false)
Allow = types.Allow
Deny = types.Deny
)

func (a Decision) String() string {
if a {
return "allow"
}
return "deny"
}

func (a Decision) MarshalJSON() ([]byte, error) { return []byte(`"` + a.String() + `"`), nil }

func (a *Decision) UnmarshalJSON(b []byte) error {
*a = string(b) == `"allow"`
return nil
}

// A Diagnostic details the errors and reasons for an authorization decision.
type Diagnostic struct {
Reasons []Reason `json:"reasons,omitempty"`
Errors []Error `json:"errors,omitempty"`
}

// An Error details the Policy index within a PolicySet, the Position within the
// text document, and the resulting error message.
type Error struct {
PolicyID PolicyID `json:"policy"`
Position Position `json:"position"`
Message string `json:"message"`
}

func (e Error) String() string {
return fmt.Sprintf("while evaluating policy `%v`: %v", e.PolicyID, e.Message)
}

// A Reason details the Policy index within a PolicySet, and the Position within
// the text document.
type Reason struct {
PolicyID PolicyID `json:"policy"`
Position Position `json:"position"`
}

// A Request is the Principal, Action, Resource, and Context portion of an
// authorization request.
type Request struct {
Principal types.EntityUID `json:"principal"`
Action types.EntityUID `json:"action"`
Resource types.EntityUID `json:"resource"`
Context types.Record `json:"context"`
}

// IsAuthorized uses the combination of the PolicySet and Entities to determine
// if the given Request to determine Decision and Diagnostic.
func (p PolicySet) IsAuthorized(entityMap types.Entities, req Request) (Decision, Diagnostic) {
c := &eval.Context{
c := eval.InitEnv(&eval.Env{
Entities: entityMap,
Principal: req.Principal,
Action: req.Action,
Resource: req.Resource,
Context: req.Context,
}
})
var diag Diagnostic
var gotForbid bool
var forbidReasons []Reason
var gotPermit bool
var permitReasons []Reason
var forbids []DiagnosticReason
var permits []DiagnosticReason
// Don't try to short circuit this.
// - Even though single forbid means forbid
// - All policy should be run to collect errors
// - For permit, all permits must be run to collect annotations
// - For forbid, forbids must be run to collect annotations
for id, po := range p.policies {
v, err := po.eval.Eval(c)
result, err := po.eval.Eval(c)
if err != nil {
diag.Errors = append(diag.Errors, Error{PolicyID: id, Position: po.Position(), Message: err.Error()})
diag.Errors = append(diag.Errors, DiagnosticError{PolicyID: id, Position: po.Position(), Message: err.Error()})
continue
}
vb, err := eval.ValueToBool(v)
if err != nil {
// should never happen, maybe remove this case
diag.Errors = append(diag.Errors, Error{PolicyID: id, Position: po.Position(), Message: err.Error()})
continue
}
if !vb {
if !result {
continue
}
if po.Effect() == Forbid {
forbidReasons = append(forbidReasons, Reason{PolicyID: id, Position: po.Position()})
gotForbid = true
forbids = append(forbids, DiagnosticReason{PolicyID: id, Position: po.Position()})
} else {
permitReasons = append(permitReasons, Reason{PolicyID: id, Position: po.Position()})
gotPermit = true
permits = append(permits, DiagnosticReason{PolicyID: id, Position: po.Position()})
}
}
if gotForbid {
diag.Reasons = forbidReasons
} else if gotPermit {
diag.Reasons = permitReasons
if len(forbids) > 0 {
diag.Reasons = forbids
return Deny, diag
}
if len(permits) > 0 {
diag.Reasons = permits
return Allow, diag
}
return Decision(gotPermit && !gotForbid), diag
return Deny, diag
}
74 changes: 16 additions & 58 deletions authorize_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package cedar

import (
"encoding/json"
"testing"

"github.com/cedar-policy/cedar-go/ast"
"github.com/cedar-policy/cedar-go/internal/eval"
"github.com/cedar-policy/cedar-go/internal/testutil"
"github.com/cedar-policy/cedar-go/types"
)
Expand Down Expand Up @@ -705,6 +702,22 @@ func TestIsAuthorized(t *testing.T) {
Want: true,
DiagErr: 0,
},
{
Name: "rfc-57", // https://github.com/cedar-policy/rfcs/blob/main/text/0057-general-multiplication.md
Policy: `permit(principal, action, resource) when { context.foo * principal.bar >= 100 };`,
Entities: types.Entities{
types.NewEntityUID("Principal", "1"): &types.Entity{
UID: types.NewEntityUID("Principal", "1"),
Attributes: types.Record{"bar": types.Long(42)},
},
},
Principal: types.NewEntityUID("Principal", "1"),
Action: types.NewEntityUID("Action", "action"),
Resource: types.NewEntityUID("Resource", "resource"),
Context: types.Record{"foo": types.Long(43)},
Want: true,
DiagErr: 0,
},
}
for _, tt := range tests {
tt := tt
Expand All @@ -723,58 +736,3 @@ func TestIsAuthorized(t *testing.T) {
})
}
}

func TestError(t *testing.T) {
t.Parallel()
e := Error{PolicyID: "policy42", Message: "bad error"}
testutil.Equals(t, e.String(), "while evaluating policy `policy42`: bad error")
}

type badEvaler struct{}

func (e *badEvaler) Eval(*eval.Context) (types.Value, error) {
return types.Long(42), nil
}

func TestBadEval(t *testing.T) {
t.Parallel()
ps := NewPolicySet()
pol := NewPolicyFromAST(ast.Permit())
pol.eval = &badEvaler{}
ps.Store("pol", pol)
dec, diag := ps.IsAuthorized(nil, Request{})
testutil.Equals(t, dec, Deny)
testutil.Equals(t, len(diag.Errors), 1)
}

func TestJSONDecision(t *testing.T) {
t.Parallel()
t.Run("MarshalAllow", func(t *testing.T) {
t.Parallel()
d := Allow
b, err := d.MarshalJSON()
testutil.OK(t, err)
testutil.Equals(t, string(b), `"allow"`)
})
t.Run("MarshalDeny", func(t *testing.T) {
t.Parallel()
d := Deny
b, err := d.MarshalJSON()
testutil.OK(t, err)
testutil.Equals(t, string(b), `"deny"`)
})
t.Run("UnmarshalAllow", func(t *testing.T) {
t.Parallel()
var d Decision
err := json.Unmarshal([]byte(`"allow"`), &d)
testutil.OK(t, err)
testutil.Equals(t, d, Allow)
})
t.Run("UnmarshalDeny", func(t *testing.T) {
t.Parallel()
var d Decision
err := json.Unmarshal([]byte(`"deny"`), &d)
testutil.OK(t, err)
testutil.Equals(t, d, Deny)
})
}
89 changes: 68 additions & 21 deletions corpus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
_ "embed"
"encoding/json"
"fmt"
"io"
"slices"
"strings"
"testing"

"github.com/cedar-policy/cedar-go"
"github.com/cedar-policy/cedar-go/internal/testutil"
"github.com/cedar-policy/cedar-go/types"
"github.com/cedar-policy/cedar-go/x/exp/batch"
)

// jsonEntity is not part of entityValue as I can find
Expand All @@ -40,14 +41,14 @@ type corpusTest struct {
ShouldValidate bool `json:"shouldValidate"`
Entities string `json:"entities"`
Requests []struct {
Desc string `json:"description"`
Principal jsonEntity `json:"principal"`
Action jsonEntity `json:"action"`
Resource jsonEntity `json:"resource"`
Context types.Record `json:"context"`
Decision string `json:"decision"`
Reasons []string `json:"reason"`
Errors []string `json:"errors"`
Desc string `json:"description"`
Principal jsonEntity `json:"principal"`
Action jsonEntity `json:"action"`
Resource jsonEntity `json:"resource"`
Context types.Record `json:"context"`
Decision types.Decision `json:"decision"`
Reasons []types.PolicyID `json:"reason"`
Errors []types.PolicyID `json:"errors"`
} `json:"requests"`
}

Expand Down Expand Up @@ -89,6 +90,7 @@ func (fdm TarFileMap) GetFileData(path string) ([]byte, error) {
return content, nil
}

//nolint:revive // due to test cognitive complexity
func TestCorpus(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -156,6 +158,13 @@ func TestCorpus(t *testing.T) {
}

for _, request := range tt.Requests {
if len(request.Reasons) == 0 && request.Reasons != nil {
request.Reasons = nil
}
if len(request.Errors) == 0 && request.Errors != nil {
request.Errors = nil
}

t.Run(request.Desc, func(t *testing.T) {
t.Parallel()
ok, diag := policySet.IsAuthorized(
Expand All @@ -167,23 +176,61 @@ func TestCorpus(t *testing.T) {
Context: request.Context,
})

if ok != (request.Decision == "allow") {
t.Fatalf("got %v want %v", ok, request.Decision)
}
var errors []string
testutil.Equals(t, ok, request.Decision)
var errors []types.PolicyID
for _, n := range diag.Errors {
errors = append(errors, string(n.PolicyID))
}
if !slices.Equal(errors, request.Errors) {
t.Errorf("errors got %v want %v", errors, request.Errors)
errors = append(errors, n.PolicyID)
}
var reasons []string
testutil.Equals(t, errors, request.Errors)
var reasons []types.PolicyID
for _, n := range diag.Reasons {
reasons = append(reasons, string(n.PolicyID))
reasons = append(reasons, n.PolicyID)
}
if !slices.Equal(reasons, request.Reasons) {
t.Errorf("reasons got %v want %v", reasons, request.Reasons)
testutil.Equals(t, reasons, request.Reasons)
})

t.Run(request.Desc+"/batch", func(t *testing.T) {
t.Parallel()
ctx := context.Background()
var res batch.Result
var total int
principal := types.EntityUID(request.Principal)
action := types.EntityUID(request.Action)
resource := types.EntityUID(request.Resource)
context := request.Context
batch.Authorize(ctx, policySet, entities, batch.Request{
Principal: batch.Variable("principal"),
Action: batch.Variable("action"),
Resource: batch.Variable("resource"),
Context: batch.Variable("context"),
Variables: batch.Variables{
"principal": []types.Value{principal},
"action": []types.Value{action},
"resource": []types.Value{resource},
"context": []types.Value{context},
},
}, func(r batch.Result) {
res = r
total++
})
testutil.Equals(t, total, 1)
testutil.Equals(t, res.Request.Principal, principal)
testutil.Equals(t, res.Request.Action, action)
testutil.Equals(t, res.Request.Resource, resource)
testutil.Equals(t, res.Request.Context, context)

ok, diag := res.Decision, res.Diagnostic
testutil.Equals(t, ok, request.Decision)
var errors []types.PolicyID
for _, n := range diag.Errors {
errors = append(errors, n.PolicyID)
}
testutil.Equals(t, errors, request.Errors)
var reasons []types.PolicyID
for _, n := range diag.Reasons {
reasons = append(reasons, n.PolicyID)
}
testutil.Equals(t, reasons, request.Reasons)
})
}
})
Expand Down
Loading

0 comments on commit efe5690

Please sign in to comment.