Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: cedar-policy/cedar-go
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.3.0
Choose a base ref
...
head repository: cedar-policy/cedar-go
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v0.3.1
Choose a head ref
Loading
34 changes: 25 additions & 9 deletions .github/workflows/corpus.yml
Original file line number Diff line number Diff line change
@@ -3,14 +3,16 @@ on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
# push:

jobs:
build:
Check-Upstream:
defaults:
run:
shell: bash
runs-on: ubuntu-latest
if: github.repository == 'cedar-policy/cedar-go'
permissions:
issues: write

steps:
- uses: actions/checkout@v4
@@ -20,19 +22,33 @@ jobs:

# cmp returns status code 1 if the files differ
- name: Compare
id: compare
run: cmp /tmp/corpus-tests.tar.gz corpus-tests.tar.gz

- name: Notify on Failure
if: failure() && github.event_name == 'schedule'
uses: actions/github-script@v6
if: failure() && steps.compare.outcome == 'failure'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.create({
// Get a list of all open issues labeled with 'upstream-corpus-test'. The documentation for
// listForRepo states that it should only return open issues.
const issues = await github.paginate(github.rest.issues.listForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
title: 'Upstream Integration Test Corpus Modified',
body: 'The upstream integration test corpus at https://raw.githubusercontent.com/cedar-policy/cedar-integration-tests/main/corpus-tests.tar.gz has been updated. Please integrate the changes into the local copy.'
assignees: ['jmccarthy', 'philhassey', 'patjakdev'],
labels: ['bug']
labels: 'upstream-corpus-test'
})
.then((issues) => {
console.log(`Found ${issues.length} open issues`)
// If one doesn't exist, create it
if (issues.length === 0) {
github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'Upstream Integration Test Corpus Modified',
body: 'The upstream integration test corpus at https://raw.githubusercontent.com/cedar-policy/cedar-integration-tests/main/corpus-tests.tar.gz has been updated. Please integrate the changes into the local copy.',
assignees: ['jmccarthy', 'philhassey', 'patjakdev'],
labels: ['upstream-corpus-test']
})
}
});
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
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -133,7 +133,18 @@ While in development (0.x.y), each tagged release may contain breaking changes.

## Change log

### New features in 0.2.x
### New features in 0.3.1

- General performance improvements to the evaluator
- An experimental batch evaluator has been added to `x/exp/batch`
- Reserved keywords are now rejected in all appropriate places when parsing Cedar text
- A parsing ambiguity between variables, entity UIDs, and extension functions has been resolved

### Upgrading from 0.2.x to 0.3.x

- The JSON marshaling of the Position struct now uses canonical lower-case keys for its fields

### New features in 0.2.0

- A programmatic AST is now available in the `ast` package.
- Policy sets can be marshaled and unmarshaled from JSON.
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"
)
@@ -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
@@ -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)
})
}
Binary file modified corpus-tests.tar.gz
Binary file not shown.
Loading