From e91a8b09ecc4c14bdd18f149bc68b7b6f25a16db Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Wed, 2 Apr 2025 14:59:04 -0300 Subject: [PATCH 01/24] feat: implement policy templates Signed-off-by: Caio Ferreira --- authorize.go | 2 +- internal/eval/compile.go | 17 +++- internal/eval/partial.go | 4 +- internal/json/json.go | 4 +- internal/json/json_marshal.go | 36 +++++-- internal/json/json_test.go | 36 +++++++ internal/json/json_unmarshal.go | 125 ++++++++++++++++++++++-- internal/json/policy_set.go | 5 +- internal/parser/cedar_marshal.go | 34 ++++++- internal/parser/cedar_parse_test.go | 12 +-- internal/parser/cedar_tokenize.go | 2 +- internal/parser/cedar_unmarshal.go | 74 ++++++++++++-- internal/parser/cedar_unmarshal_test.go | 60 +++++++++++- internal/parser/policy.go | 11 ++- internal/parser/template.go | 109 +++++++++++++++++++++ internal/parser/template_test.go | 73 ++++++++++++++ policy_list.go | 45 +++++++-- policy_set.go | 91 ++++++++++++----- policy_set_test.go | 8 +- template.go | 56 +++++++++++ template_test.go | 124 +++++++++++++++++++++++ types/entity.go | 13 +++ types/entity_uid.go | 2 + types/template.go | 12 +++ x/exp/ast/policy.go | 32 ++++++ x/exp/ast/scope.go | 62 +++++++++--- x/exp/batch/batch.go | 4 +- 27 files changed, 957 insertions(+), 96 deletions(-) create mode 100644 internal/parser/template.go create mode 100644 internal/parser/template_test.go create mode 100644 template.go create mode 100644 template_test.go create mode 100644 types/template.go diff --git a/authorize.go b/authorize.go index c14e92a7..c02fa508 100644 --- a/authorize.go +++ b/authorize.go @@ -38,7 +38,7 @@ func (p PolicySet) IsAuthorized(entities types.EntityGetter, req Request) (Decis // - 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 { + for id, po := range p.policies.StaticPolicies { result, err := po.eval.Eval(env) if err != nil { diag.Errors = append(diag.Errors, DiagnosticError{PolicyID: id, Position: po.Position(), Message: err.Error()}) diff --git a/internal/eval/compile.go b/internal/eval/compile.go index 599c349c..15419c94 100644 --- a/internal/eval/compile.go +++ b/internal/eval/compile.go @@ -66,9 +66,9 @@ func scopeToNode(varNode ast.NodeTypeVariable, in ast.IsScopeNode) ast.Node { case ast.ScopeTypeAll: return ast.True() case ast.ScopeTypeEq: - return ast.NewNode(varNode).Equal(ast.Value(t.Entity)) + return ast.NewNode(varNode).Equal(ast.Value(entityReferenceToUID(t.Entity))) case ast.ScopeTypeIn: - return ast.NewNode(varNode).In(ast.Value(t.Entity)) + return ast.NewNode(varNode).In(ast.Value(entityReferenceToUID(t.Entity))) case ast.ScopeTypeInSet: vals := make([]types.Value, len(t.Entities)) for i, e := range t.Entities { @@ -79,8 +79,19 @@ func scopeToNode(varNode ast.NodeTypeVariable, in ast.IsScopeNode) ast.Node { return ast.NewNode(varNode).Is(t.Type) case ast.ScopeTypeIsIn: - return ast.NewNode(varNode).IsIn(t.Type, ast.Value(t.Entity)) + return ast.NewNode(varNode).IsIn(t.Type, ast.Value(entityReferenceToUID(t.Entity))) default: panic(fmt.Sprintf("unknown scope type %T", t)) } } + +func entityReferenceToUID(ef types.EntityReference) types.EntityUID { + switch e := ef.(type) { + case types.EntityUID: + return e + case types.VariableSlot: + panic("variable slot cannot be evaluated, you should instantiate a template-linked policy first") + default: + panic(fmt.Sprintf("unknown entity reference type %T", e)) + } +} diff --git a/internal/eval/partial.go b/internal/eval/partial.go index 4a065365..408fc43e 100644 --- a/internal/eval/partial.go +++ b/internal/eval/partial.go @@ -139,14 +139,14 @@ func partialScopeEval(env Env, ent types.Value, in ast.IsScopeNode) (evaled bool case ast.ScopeTypeEq: return true, e == t.Entity case ast.ScopeTypeIn: - return true, entityInOne(env, e, t.Entity) + return true, entityInOne(env, e, entityReferenceToUID(t.Entity)) case ast.ScopeTypeInSet: set := mapset.Immutable(t.Entities...) return true, entityInSet(env, e, set) case ast.ScopeTypeIs: return true, e.Type == t.Type case ast.ScopeTypeIsIn: - return true, e.Type == t.Type && entityInOne(env, e, t.Entity) + return true, e.Type == t.Type && entityInOne(env, e, entityReferenceToUID(t.Entity)) default: panic(fmt.Sprintf("unknown scope type %T", t)) } diff --git a/internal/json/json.go b/internal/json/json.go index 0f748b30..9c29d7f6 100644 --- a/internal/json/json.go +++ b/internal/json/json.go @@ -17,7 +17,8 @@ type policyJSON struct { // scopeInJSON uses the implicit form of EntityUID JSON serialization to match the Rust SDK type scopeInJSON struct { - Entity types.ImplicitlyMarshaledEntityUID `json:"entity"` + Entity *types.ImplicitlyMarshaledEntityUID `json:"entity,omitempty"` + Slot *string `json:"slot,omitempty"` } // scopeJSON uses the implicit form of EntityUID JSON serialization to match the Rust SDK @@ -27,6 +28,7 @@ type scopeJSON struct { Entities []types.ImplicitlyMarshaledEntityUID `json:"entities,omitempty"` EntityType string `json:"entity_type,omitempty"` In *scopeInJSON `json:"in,omitempty"` + Slot *string `json:"slot,omitempty"` } type conditionJSON struct { diff --git a/internal/json/json_marshal.go b/internal/json/json_marshal.go index e1108e86..1acc6d1c 100644 --- a/internal/json/json_marshal.go +++ b/internal/json/json_marshal.go @@ -15,13 +15,27 @@ func (s *scopeJSON) FromNode(src ast.IsScopeNode) { return case ast.ScopeTypeEq: s.Op = "==" - e := types.ImplicitlyMarshaledEntityUID(t.Entity) - s.Entity = &e + switch ent := t.Entity.(type) { + case types.EntityUID: + e := types.ImplicitlyMarshaledEntityUID(ent) + s.Entity = &e + case types.VariableSlot: + varName := ent.ID.String() + s.Slot = &varName + } + return case ast.ScopeTypeIn: s.Op = "in" - e := types.ImplicitlyMarshaledEntityUID(t.Entity) - s.Entity = &e + switch ent := t.Entity.(type) { + case types.EntityUID: + e := types.ImplicitlyMarshaledEntityUID(ent) + s.Entity = &e + case types.VariableSlot: + varName := ent.ID.String() + s.Slot = &varName + } + return case ast.ScopeTypeInSet: s.Op = "in" @@ -38,9 +52,19 @@ func (s *scopeJSON) FromNode(src ast.IsScopeNode) { case ast.ScopeTypeIsIn: s.Op = "is" s.EntityType = string(t.Type) - s.In = &scopeInJSON{ - Entity: types.ImplicitlyMarshaledEntityUID(t.Entity), + in := &scopeInJSON{} + + switch et := t.Entity.(type) { + case types.EntityUID: + uid := types.ImplicitlyMarshaledEntityUID(et) + in.Entity = &uid + case types.VariableSlot: + varName := et.ID.String() + in.Slot = &varName } + + s.In = in + return default: panic(fmt.Sprintf("unknown scope type %T", t)) diff --git a/internal/json/json_test.go b/internal/json/json_test.go index 85e06441..e38900b2 100644 --- a/internal/json/json_test.go +++ b/internal/json/json_test.go @@ -468,6 +468,42 @@ func TestUnmarshalJSON(t *testing.T) { ast.Permit().When(ast.ExtensionCall("ip", ast.String("10.0.0.43")).IsInRange(ast.ExtensionCall("ip", ast.String("10.0.0.42/8")))), testutil.OK, }, + { + "principal template variable", + `{"effect":"permit","principal":{"op":"==", "slot": "?principal"},"action":{"op":"All"},"resource":{"op":"All"}}`, + ast.Permit().PrincipalEq(types.VariableSlot{ID: types.PrincipalSlot}).AddSlot(types.PrincipalSlot), + testutil.OK, + }, + { + "principal template variable with in operator", + `{"effect":"permit","principal":{"op":"in", "slot": "?principal"},"action":{"op":"All"},"resource":{"op":"All"}}`, + ast.Permit().PrincipalIn(types.VariableSlot{ID: types.PrincipalSlot}).AddSlot(types.PrincipalSlot), + testutil.OK, + }, + { + "principal template variable with is in operator", + `{"effect":"permit","principal":{"op":"is", "entity_type": "User", "in": {"slot": "?principal"} },"action":{"op":"All"},"resource":{"op":"All"}}`, + ast.Permit().PrincipalIsIn("User", types.VariableSlot{ID: types.PrincipalSlot}).AddSlot(types.PrincipalSlot), + testutil.OK, + }, + { + "resource template variable", + `{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"==", "slot": "?resource"}}`, + ast.Permit().ResourceEq(types.VariableSlot{ID: types.ResourceSlot}).AddSlot(types.ResourceSlot), + testutil.OK, + }, + { + "resource template variable with in operator", + `{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"in", "slot": "?resource"}}`, + ast.Permit().ResourceIn(types.VariableSlot{ID: types.ResourceSlot}).AddSlot(types.ResourceSlot), + testutil.OK, + }, + { + "resource template variable with is in operator", + `{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"is", "entity_type": "Photo", "in": {"slot": "?resource"} }}`, + ast.Permit().ResourceIsIn("Photo", types.VariableSlot{ID: types.ResourceSlot}).AddSlot(types.ResourceSlot), + testutil.OK, + }, } for _, tt := range tests { diff --git a/internal/json/json_unmarshal.go b/internal/json/json_unmarshal.go index ac3be591..3da1eb6b 100644 --- a/internal/json/json_unmarshal.go +++ b/internal/json/json_unmarshal.go @@ -15,6 +15,68 @@ import ( type isPrincipalResourceScopeNode interface { ast.IsPrincipalScopeNode ast.IsResourceScopeNode + Slot() (types.SlotID, bool) +} + +func slotID(id *string) (types.SlotID, error) { + sid := *id + + switch sid { + case string(types.PrincipalSlot): + return types.PrincipalSlot, nil + case string(types.ResourceSlot): + return types.ResourceSlot, nil + default: + return "", fmt.Errorf("unknown slot ID: %v", sid) + } +} + +func scopeEntityReference(s *scopeJSON) (types.EntityReference, error) { + var ref types.EntityReference + + if s.Entity == nil && s.Slot == nil { + return nil, fmt.Errorf("entity or slot should be set") + } + + switch { + case s.Slot != nil: + id, err := slotID(s.Slot) + if err != nil { + return nil, err + } + + ref = types.VariableSlot{ID: id} + case s.Entity != nil: + ref = types.EntityUID(*s.Entity) + default: + return nil, fmt.Errorf("missing entity and slot") + } + + return ref, nil +} + +func scopeInEntityReference(s *scopeInJSON) (types.EntityReference, error) { + var ref types.EntityReference + + if s.Entity == nil && s.Slot == nil { + return nil, fmt.Errorf("entity or slot should be set") + } + + switch { + case s.Slot != nil: + id, err := slotID(s.Slot) + if err != nil { + return nil, err + } + + ref = types.VariableSlot{ID: id} + case s.Entity != nil: + ref = types.EntityUID(*s.Entity) + default: + return nil, fmt.Errorf("missing entity and slot") + } + + return ref, nil } func (s *scopeJSON) ToPrincipalResourceNode() (isPrincipalResourceScopeNode, error) { @@ -22,21 +84,43 @@ func (s *scopeJSON) ToPrincipalResourceNode() (isPrincipalResourceScopeNode, err case "All": return ast.Scope{}.All(), nil case "==": - if s.Entity == nil { - return nil, fmt.Errorf("missing entity") + var ref types.EntityReference + + switch { + case s.Slot != nil: + id, err := slotID(s.Slot) + if err != nil { + return nil, err + } + + ref = types.VariableSlot{ID: id} + case s.Entity != nil: + ref = types.EntityUID(*s.Entity) + default: + return nil, fmt.Errorf("missing entity and slot") } - return ast.Scope{}.Eq(types.EntityUID(*s.Entity)), nil + + return ast.Scope{}.Eq(ref), nil case "in": - if s.Entity == nil { - return nil, fmt.Errorf("missing entity") + ref, err := scopeEntityReference(s) + if err != nil { + return nil, err } - return ast.Scope{}.In(types.EntityUID(*s.Entity)), nil + + return ast.Scope{}.In(ref), nil case "is": if s.In == nil { return ast.Scope{}.Is(types.EntityType(s.EntityType)), nil } - return ast.Scope{}.IsIn(types.EntityType(s.EntityType), types.EntityUID(s.In.Entity)), nil + + ref, err := scopeInEntityReference(s.In) + if err != nil { + return nil, err + } + + return ast.Scope{}.IsIn(types.EntityType(s.EntityType), ref), nil } + return nil, fmt.Errorf("unknown op: %v", s.Op) } @@ -304,19 +388,40 @@ func (p *Policy) UnmarshalJSON(b []byte) error { for k, v := range j.Annotations { p.unwrap().Annotate(types.Ident(k), types.String(v)) } - var err error - p.Principal, err = j.Principal.ToPrincipalResourceNode() + + principal, err := j.Principal.ToPrincipalResourceNode() if err != nil { return fmt.Errorf("error in principal: %w", err) } + + p.Principal = principal + if slot, found := principal.Slot(); found { + if slot != types.PrincipalSlot { + return fmt.Errorf("variable used in principal slot is not %s", types.PrincipalSlot) + } + + p.unwrap().AddSlot(slot) + } + p.Action, err = j.Action.ToActionNode() if err != nil { return fmt.Errorf("error in action: %w", err) } - p.Resource, err = j.Resource.ToPrincipalResourceNode() + + resource, err := j.Resource.ToPrincipalResourceNode() if err != nil { return fmt.Errorf("error in resource: %w", err) } + + p.Resource = resource + if slot, found := resource.Slot(); found { + if slot != types.ResourceSlot { + return fmt.Errorf("variable used in resource slot is not %s", types.ResourceSlot) + } + + p.unwrap().AddSlot(slot) + } + for _, c := range j.Conditions { n, err := c.Body.ToNode() if err != nil { diff --git a/internal/json/policy_set.go b/internal/json/policy_set.go index 39ec3c5b..6bc2793b 100644 --- a/internal/json/policy_set.go +++ b/internal/json/policy_set.go @@ -2,6 +2,9 @@ package json type PolicySet map[string]*Policy +type TemplateSet map[string]*Policy + type PolicySetJSON struct { - StaticPolicies PolicySet `json:"staticPolicies"` + StaticPolicies PolicySet `json:"staticPolicies"` + Templates TemplateSet `json:"templates"` } diff --git a/internal/parser/cedar_marshal.go b/internal/parser/cedar_marshal.go index c462a263..7f3d2045 100644 --- a/internal/parser/cedar_marshal.go +++ b/internal/parser/cedar_marshal.go @@ -2,7 +2,9 @@ package parser import ( "bytes" + "errors" "fmt" + "github.com/cedar-policy/cedar-go/types" "github.com/cedar-policy/cedar-go/internal/consts" "github.com/cedar-policy/cedar-go/internal/extensions" @@ -33,9 +35,19 @@ func scopeToNode(varNode ast.NodeTypeVariable, in ast.IsScopeNode) ast.Node { case ast.ScopeTypeAll: return ast.True() case ast.ScopeTypeEq: - return ast.NewNode(varNode).Equal(ast.Value(t.Entity)) + rhs, err := entityReferenceToNode(t.Entity) + if err != nil { + panic(err) + } + + return ast.NewNode(varNode).Equal(rhs) case ast.ScopeTypeIn: - return ast.NewNode(varNode).In(ast.Value(t.Entity)) + rhs, err := entityReferenceToNode(t.Entity) + if err != nil { + panic(err) + } + + return ast.NewNode(varNode).In(rhs) case ast.ScopeTypeInSet: set := make([]ast.Node, len(t.Entities)) for i, e := range t.Entities { @@ -46,12 +58,28 @@ func scopeToNode(varNode ast.NodeTypeVariable, in ast.IsScopeNode) ast.Node { return ast.NewNode(varNode).Is(t.Type) case ast.ScopeTypeIsIn: - return ast.NewNode(varNode).IsIn(t.Type, ast.Value(t.Entity)) + rhs, err := entityReferenceToNode(t.Entity) + if err != nil { + panic(err) + } + + return ast.NewNode(varNode).IsIn(t.Type, rhs) default: panic(fmt.Sprintf("unknown scope type %T", t)) } } +func entityReferenceToNode(ef types.EntityReference) (ast.Node, error) { + switch e := ef.(type) { + case types.EntityUID: + return ast.Value(e), nil + case types.VariableSlot: + return ast.NewNode(ast.NodeTypeVariable{Name: types.String(e.ID)}), nil + default: + return ast.Node{}, errors.New("unknown entity reference type") + } +} + func (p *Policy) marshalScope(buf *bytes.Buffer) { _, principalAll := p.Principal.(ast.ScopeTypeAll) _, actionAll := p.Action.(ast.ScopeTypeAll) diff --git a/internal/parser/cedar_parse_test.go b/internal/parser/cedar_parse_test.go index f72663c8..0b3b495a 100644 --- a/internal/parser/cedar_parse_test.go +++ b/internal/parser/cedar_parse_test.go @@ -331,7 +331,7 @@ func TestParse(t *testing.T) { // N.B. Until we support the re-rendering of comments, we have to ignore the position for the purposes of // these tests (see test "ex1") - for _, pp := range policies { + for _, pp := range policies.StaticPolicies { pp.Position = ast.Position{Offset: 0, Line: 1, Column: 1} var buf bytes.Buffer @@ -341,7 +341,7 @@ func TestParse(t *testing.T) { err = p2.UnmarshalCedar(buf.Bytes()) testutil.OK(t, err) - testutil.Equals(t, p2[0], pp) + testutil.Equals(t, p2.StaticPolicies[0], pp) } }) } @@ -364,8 +364,8 @@ permit( principal, action, resource ); var out parser.PolicySlice err := out.UnmarshalCedar([]byte(in)) testutil.OK(t, err) - testutil.Equals(t, len(out), 3) - testutil.Equals(t, out[0].Position, ast.Position{Offset: 17, Line: 2, Column: 1}) - testutil.Equals(t, out[1].Position, ast.Position{Offset: 86, Line: 7, Column: 3}) - testutil.Equals(t, out[2].Position, ast.Position{Offset: 148, Line: 10, Column: 2}) + testutil.Equals(t, len(out.StaticPolicies), 3) + testutil.Equals(t, out.StaticPolicies[0].Position, ast.Position{Offset: 17, Line: 2, Column: 1}) + testutil.Equals(t, out.StaticPolicies[1].Position, ast.Position{Offset: 86, Line: 7, Column: 3}) + testutil.Equals(t, out.StaticPolicies[2].Position, ast.Position{Offset: 148, Line: 10, Column: 2}) } diff --git a/internal/parser/cedar_tokenize.go b/internal/parser/cedar_tokenize.go index 11bca615..3d9b75f6 100644 --- a/internal/parser/cedar_tokenize.go +++ b/internal/parser/cedar_tokenize.go @@ -372,7 +372,7 @@ func (s *scanner) scanComment(ch rune) rune { func (s *scanner) scanOperator(ch0, ch rune) (TokenType, rune) { switch ch0 { - case '@', '.', ',', ';', '(', ')', '{', '}', '[', ']', '+', '-', '*': + case '@', '.', ',', ';', '(', ')', '{', '}', '[', ']', '+', '-', '*', '?': case ':': if ch == ':' { ch = s.next() diff --git a/internal/parser/cedar_unmarshal.go b/internal/parser/cedar_unmarshal.go index 6811f582..c4233c3b 100644 --- a/internal/parser/cedar_unmarshal.go +++ b/internal/parser/cedar_unmarshal.go @@ -18,7 +18,9 @@ func (p *PolicySlice) UnmarshalCedar(b []byte) error { return err } - var policySet PolicySlice + var policySet []*Policy + var templateSet []*Template + parser := newParser(tokens) for !parser.peek().isEOF() { var policy Policy @@ -26,10 +28,17 @@ func (p *PolicySlice) UnmarshalCedar(b []byte) error { return err } - policySet = append(policySet, &policy) + if len(policy.unwrap().Slots()) > 0 { + t := Template(policy) + templateSet = append(templateSet, &t) + } else { + policySet = append(policySet, &policy) + } } - *p = policySet + p.StaticPolicies = policySet + p.Templates = templateSet + return nil } @@ -184,6 +193,13 @@ func (p *parser) effect(a *ast.Annotations) (*ast.Policy, error) { return nil, p.errorf("unexpected effect: %v", next.Text) } +func addSlotToPolicy(entRef types.EntityReference, p *ast.Policy) { + switch varSlot := entRef.(type) { + case types.VariableSlot: + p.AddSlot(varSlot.ID) + } +} + func (p *parser) principal(policy *ast.Policy) error { if err := p.exact(consts.Principal); err != nil { return err @@ -191,10 +207,12 @@ func (p *parser) principal(policy *ast.Policy) error { switch p.peek().Text { case "==": p.advance() - entity, err := p.entity() + entity, err := p.entityReference() if err != nil { return err } + + addSlotToPolicy(entity, policy) policy.PrincipalEq(entity) return nil case "is": @@ -205,10 +223,12 @@ func (p *parser) principal(policy *ast.Policy) error { } if p.peek().Text == "in" { p.advance() - entity, err := p.entity() + entity, err := p.entityReference() if err != nil { return err } + + addSlotToPolicy(entity, policy) policy.PrincipalIsIn(path, entity) return nil } @@ -217,10 +237,12 @@ func (p *parser) principal(policy *ast.Policy) error { return nil case "in": p.advance() - entity, err := p.entity() + entity, err := p.entityReference() if err != nil { return err } + + addSlotToPolicy(entity, policy) policy.PrincipalIn(entity) return nil } @@ -230,10 +252,38 @@ func (p *parser) principal(policy *ast.Policy) error { func (p *parser) entity() (types.EntityUID, error) { var res types.EntityUID + t := p.advance() if !t.isIdent() { return res, p.errorf("expected ident") } + + return p.entityFirstPathPreread(types.EntityType(t.Text)) +} + +func (p *parser) entityReference() (types.EntityReference, error) { + var res types.EntityUID + + if p.peek().Type == TokenOperator && p.peek().Text == "?" { + p.advance() // consume `?` + t := p.advance() + + varName := "?" + t.Text + switch varName { + case string(types.PrincipalSlot): + return types.VariableSlot{ID: types.PrincipalSlot}, nil + case string(types.ResourceSlot): + return types.VariableSlot{ID: types.ResourceSlot}, nil + } + + return nil, p.errorf("unknown variable name %v", varName) + } + + t := p.advance() + if !t.isIdent() { + return res, p.errorf("expected ident") + } + return p.entityFirstPathPreread(types.EntityType(t.Text)) } @@ -347,10 +397,12 @@ func (p *parser) resource(policy *ast.Policy) error { switch p.peek().Text { case "==": p.advance() - entity, err := p.entity() + entity, err := p.entityReference() if err != nil { return err } + + addSlotToPolicy(entity, policy) policy.ResourceEq(entity) return nil case "is": @@ -361,10 +413,12 @@ func (p *parser) resource(policy *ast.Policy) error { } if p.peek().Text == "in" { p.advance() - entity, err := p.entity() + entity, err := p.entityReference() if err != nil { return err } + + addSlotToPolicy(entity, policy) policy.ResourceIsIn(path, entity) return nil } @@ -373,10 +427,12 @@ func (p *parser) resource(policy *ast.Policy) error { return nil case "in": p.advance() - entity, err := p.entity() + entity, err := p.entityReference() if err != nil { return err } + + addSlotToPolicy(entity, policy) policy.ResourceIn(entity) return nil } diff --git a/internal/parser/cedar_unmarshal_test.go b/internal/parser/cedar_unmarshal_test.go index b8ae8f41..bd9e471c 100644 --- a/internal/parser/cedar_unmarshal_test.go +++ b/internal/parser/cedar_unmarshal_test.go @@ -465,6 +465,60 @@ when { (if true then 2 else 3 * 4) == 2 };`, when { (if true then 2 else 3) * 4 == 8 };`, ast.Permit().When(ast.IfThenElse(ast.True(), ast.Long(2), ast.Long(3)).Multiply(ast.Long(4)).Equal(ast.Long(8))), }, + { + "principal variable", + `permit ( + principal == ?principal, + action, + resource +);`, + ast.Permit().PrincipalEq(types.VariableSlot{ID: types.PrincipalSlot}).AddSlot(types.PrincipalSlot), + }, + { + "principal template variable with in operator", + `permit ( + principal in ?principal, + action, + resource +);`, + ast.Permit().PrincipalIn(types.VariableSlot{ID: types.PrincipalSlot}).AddSlot(types.PrincipalSlot), + }, + { + "principal template variable with is in operator", + `permit ( + principal is User in ?principal, + action, + resource +);`, + ast.Permit().PrincipalIsIn("User", types.VariableSlot{ID: types.PrincipalSlot}).AddSlot(types.PrincipalSlot), + }, + { + "resource template variable", + `permit ( + principal, + action, + resource == ?resource +);`, + ast.Permit().ResourceEq(types.VariableSlot{ID: types.ResourceSlot}).AddSlot(types.ResourceSlot), + }, + { + "resource template variable with in operator", + `permit ( + principal, + action, + resource in ?resource +);`, + ast.Permit().ResourceIn(types.VariableSlot{ID: types.ResourceSlot}).AddSlot(types.ResourceSlot), + }, + { + "resource template variable with is in operator", + `permit ( + principal, + action, + resource is Photo in ?resource +);`, + ast.Permit().ResourceIsIn("Photo", types.VariableSlot{ID: types.ResourceSlot}).AddSlot(types.ResourceSlot), + }, } for _, tt := range parseTests { @@ -537,7 +591,7 @@ func TestParsePolicySet(t *testing.T) { expectedPolicy := ast.Permit() expectedPolicy.Position = ast.Position{Offset: 0, Line: 1, Column: 1} - testutil.Equals(t, policies[0], (*parser.Policy)(expectedPolicy)) + testutil.Equals(t, policies.StaticPolicies[0], (*parser.Policy)(expectedPolicy)) }) t.Run("two policies", func(t *testing.T) { policyStr := []byte(`permit ( @@ -555,11 +609,11 @@ func TestParsePolicySet(t *testing.T) { expectedPolicy0 := ast.Permit() expectedPolicy0.Position = ast.Position{Offset: 0, Line: 1, Column: 1} - testutil.Equals(t, policies[0], (*parser.Policy)(expectedPolicy0)) + testutil.Equals(t, policies.StaticPolicies[0], (*parser.Policy)(expectedPolicy0)) expectedPolicy1 := ast.Forbid() expectedPolicy1.Position = ast.Position{Offset: 53, Line: 6, Column: 3} - testutil.Equals(t, policies[1], (*parser.Policy)(expectedPolicy1)) + testutil.Equals(t, policies.StaticPolicies[1], (*parser.Policy)(expectedPolicy1)) }) } diff --git a/internal/parser/policy.go b/internal/parser/policy.go index b625cf13..a44b7eea 100644 --- a/internal/parser/policy.go +++ b/internal/parser/policy.go @@ -2,5 +2,14 @@ package parser import "github.com/cedar-policy/cedar-go/x/exp/ast" -type PolicySlice []*Policy type Policy ast.Policy + +func (p *Policy) unwrap() *ast.Policy { + return (*ast.Policy)(p) +} + +//todo: fix +type PolicySlice struct { + StaticPolicies []*Policy + Templates []*Template +} diff --git a/internal/parser/template.go b/internal/parser/template.go new file mode 100644 index 00000000..ffff65be --- /dev/null +++ b/internal/parser/template.go @@ -0,0 +1,109 @@ +package parser + +import ( + "encoding/json" + "fmt" + "github.com/cedar-policy/cedar-go/x/exp/ast" + "github.com/cedar-policy/cedar-go/types" +) + +type Template ast.Policy + +func (p *Template) ClonePolicy() *Policy { + clone := (*ast.Policy)(p).Clone() + parserPolicy := Policy(clone) + + return &parserPolicy +} + +type LinkedPolicy struct { + TemplateID string + LinkID string + Template *Template + + slotEnv map[types.SlotID]types.EntityUID +} + +// NewLinkedPolicy creates a new instance of LinkedPolicy. +func NewLinkedPolicy(template *Template, templateID string, linkID string, slotEnv map[types.SlotID]types.EntityUID) LinkedPolicy { + return LinkedPolicy{ + Template: template, + TemplateID: templateID, + LinkID: linkID, + slotEnv: slotEnv, + } +} + +func (p LinkedPolicy) Render() (Policy, error) { + body := p.Template.ClonePolicy().unwrap() + + if len(body.Slots()) != len(p.slotEnv) { + return Policy{}, fmt.Errorf("slot env length %d does not match template slot length %d", len(p.slotEnv), len(body.Slots())) + } + + for _, slot := range body.Slots() { + switch slot { + case types.PrincipalSlot: + body.Principal = linkScope(body.Principal, p.slotEnv) + case types.ResourceSlot: + body.Resource = linkScope(body.Resource, p.slotEnv) + default: + return Policy{}, fmt.Errorf("unknown variable %s", slot) + } + } + + return Policy(*body), nil +} + +func linkScope[T ast.IsScopeNode](scope T, slotEnv map[types.SlotID]types.EntityUID) T { + var linkedScope any = scope + + switch t := any(scope).(type) { + case ast.ScopeTypeEq: + t.Entity = resolveSlot(t.Entity, slotEnv) + + linkedScope = t + case ast.ScopeTypeIn: + t.Entity = resolveSlot(t.Entity, slotEnv) + + linkedScope = t + case ast.ScopeTypeIsIn: + t.Entity = resolveSlot(t.Entity, slotEnv) + + linkedScope = t + default: + panic(fmt.Sprintf("unknown scope type %T", t)) + } + + return linkedScope.(T) +} + +func resolveSlot(ef types.EntityReference, slotEnv map[types.SlotID]types.EntityUID) types.EntityReference { + switch e := ef.(type) { + case types.EntityUID: + return e + case types.VariableSlot: + return slotEnv[e.ID] + default: + panic(fmt.Sprintf("unknown entity reference type %T", e)) + } +} + +// MarshalJSON marshals a LinkedPolicy to JSON following cedar-cli format. +func (p LinkedPolicy) MarshalJSON() ([]byte, error) { + lp := struct { + TemplateID string `json:"template_id"` + LinkID string `json:"link_id"` + Args map[string]string `json:"args"` + }{ + TemplateID: p.TemplateID, + LinkID: p.LinkID, + } + + lp.Args = make(map[string]string, len(p.slotEnv)) + for k, v := range p.slotEnv { + lp.Args[string(k)] = v.String() + } + + return json.Marshal(lp) +} diff --git a/internal/parser/template_test.go b/internal/parser/template_test.go new file mode 100644 index 00000000..b7021a52 --- /dev/null +++ b/internal/parser/template_test.go @@ -0,0 +1,73 @@ +package parser_test + +import ( + "github.com/cedar-policy/cedar-go/internal/parser" + "github.com/cedar-policy/cedar-go/internal/testutil" + "github.com/cedar-policy/cedar-go/types" + "github.com/cedar-policy/cedar-go/x/exp/ast" + "testing" +) + +func TestLinkTemplateToPolicy(t *testing.T) { + linkTests := []struct { + Name string + TemplateString string + TemplateID string + LinkID string + Env map[types.SlotID]types.EntityUID + Want parser.Policy + }{ + { + "principal variable", + `permit ( + principal == ?principal, + action, + resource +);`, + "principal_test", + "principal_link", + map[types.SlotID]types.EntityUID{"?principal": types.NewEntityUID("User", "alice")}, + parserPolicy(ast.Permit(). + PrincipalEq(types.EntityUID{Type: "User", ID: "alice"}). + AddSlot(types.PrincipalSlot)), + }, + { + "resource variable", + `permit ( + principal, + action, + resource == ?resource +);`, + "resource_test", + "resource_link", + map[types.SlotID]types.EntityUID{"?resource": types.NewEntityUID("Album", "trip")}, + parserPolicy(ast.Permit(). + ResourceEq(types.EntityUID{Type: "Album", ID: "trip"}). + AddSlot(types.ResourceSlot)), + }, + } + + for _, tt := range linkTests { + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + + var templateBody parser.Policy + testutil.OK(t, templateBody.UnmarshalCedar([]byte(tt.TemplateString))) + template := parser.Template(templateBody) + + linkedPolicy := parser.NewLinkedPolicy(&template, tt.TemplateID, tt.LinkID, tt.Env) + + testutil.Equals(t, linkedPolicy.LinkID, tt.LinkID) + + newPolicy, err := linkedPolicy.Render() + testutil.OK(t, err) + + newPolicy.Position = ast.Position{} + testutil.Equals(t, newPolicy, tt.Want) + }) + } +} + +func parserPolicy(inAST *ast.Policy) parser.Policy { + return parser.Policy(*inAST) +} diff --git a/policy_list.go b/policy_list.go index 835d170d..bd118cad 100644 --- a/policy_list.go +++ b/policy_list.go @@ -10,18 +10,22 @@ import ( // PolicyList represents a list of un-named Policy's. Cedar documents, unlike the PolicySet form, don't have a means of // naming individual policies. -type PolicyList []*Policy +type PolicyList struct { + StaticPolicies []*Policy + Templates []*Template +} // NewPolicyListFromBytes will create a Policies from the given text document with the given file name used in Position // data. If there is an error parsing the document, it will be returned. func NewPolicyListFromBytes(fileName string, document []byte) (PolicyList, error) { var policySlice PolicyList if err := policySlice.UnmarshalCedar(document); err != nil { - return nil, err + return PolicyList{}, err } - for _, p := range policySlice { + for _, p := range policySlice.StaticPolicies { p.SetFilename(fileName) } + //todo: set template filename return policySlice, nil } @@ -32,24 +36,47 @@ func (p *PolicyList) UnmarshalCedar(b []byte) error { if err := res.UnmarshalCedar(b); err != nil { return fmt.Errorf("parser error: %w", err) } - policySlice := make([]*Policy, 0, len(res)) - for _, p := range res { + + staticPolicies := make([]*Policy, 0, len(res.StaticPolicies)) + for _, p := range res.StaticPolicies { newPolicy := newPolicy((*internalast.Policy)(p)) - policySlice = append(policySlice, newPolicy) + staticPolicies = append(staticPolicies, newPolicy) + } + + templates := make([]*Template, 0, len(res.Templates)) + for _, p := range res.Templates { + t := Template(*p) + templates = append(templates, &t) } - *p = policySlice + + p.StaticPolicies = staticPolicies + p.Templates = templates + return nil } // MarshalCedar emits a concatenated Cedar representation of the policies. func (p PolicyList) MarshalCedar() []byte { var buf bytes.Buffer - for i, policy := range p { + for i, policy := range p.StaticPolicies { buf.Write(policy.MarshalCedar()) - if i < len(p)-1 { + if i < len(p.StaticPolicies)-1 { + buf.WriteString("\n\n") + } + } + + if len(p.Templates) > 0 { + buf.WriteString("\n\n") + } + + for i, template := range p.Templates { + buf.Write(template.MarshalCedar()) + + if i < len(p.Templates)-1 { buf.WriteString("\n\n") } } + return buf.Bytes() } diff --git a/policy_set.go b/policy_set.go index 2695c4bd..0ba3349e 100644 --- a/policy_set.go +++ b/policy_set.go @@ -16,7 +16,17 @@ import ( type PolicyID = types.PolicyID // PolicyMap is a map of policy IDs to policy -type PolicyMap map[PolicyID]*Policy +type PolicyMap struct { + StaticPolicies map[PolicyID]*Policy + Templates map[PolicyID]*Template +} + +func makePolicyMap() PolicyMap { + return PolicyMap{ + StaticPolicies: make(map[PolicyID]*Policy), + Templates: make(map[PolicyID]*Template), + } +} // PolicySet is a set of named policies against which a request can be authorized. type PolicySet struct { @@ -26,7 +36,7 @@ type PolicySet struct { // NewPolicySet creates a new, empty PolicySet func NewPolicySet() *PolicySet { - return &PolicySet{policies: PolicyMap{}} + return &PolicySet{policies: makePolicyMap()} } // NewPolicySetFromBytes will create a PolicySet from the given text document with the given file name used in Position @@ -37,48 +47,73 @@ func NewPolicySet() *PolicySet { func NewPolicySetFromBytes(fileName string, document []byte) (*PolicySet, error) { policySlice, err := NewPolicyListFromBytes(fileName, document) if err != nil { - return &PolicySet{}, err + return nil, err + } + + pm := PolicyMap{ + StaticPolicies: make(map[PolicyID]*Policy, len(policySlice.StaticPolicies)), + Templates: make(map[PolicyID]*Template, len(policySlice.Templates)), } - policyMap := make(PolicyMap, len(policySlice)) - for i, p := range policySlice { + + for i, p := range policySlice.StaticPolicies { policyID := PolicyID(fmt.Sprintf("policy%d", i)) - policyMap[policyID] = p + pm.StaticPolicies[policyID] = p + } + + for i, t := range policySlice.Templates { + policyID := PolicyID(fmt.Sprintf("template%d", i)) + pm.Templates[policyID] = t } - return &PolicySet{policies: policyMap}, nil + + return &PolicySet{policies: pm}, nil } // Get returns the Policy with the given ID. If a policy with the given ID // does not exist, nil is returned. func (p PolicySet) Get(policyID PolicyID) *Policy { - return p.policies[policyID] + return p.policies.StaticPolicies[policyID] } +//todo: add support for templates in Add and Remove + // Add inserts or updates a policy with the given ID. Returns true if a policy // with the given ID did not already exist in the set. func (p *PolicySet) Add(policyID PolicyID, policy *Policy) bool { - _, exists := p.policies[policyID] - p.policies[policyID] = policy + _, exists := p.policies.StaticPolicies[policyID] + p.policies.StaticPolicies[policyID] = policy return !exists } // Remove removes a policy from the PolicySet. Returns true if a policy with // the given ID already existed in the set. func (p *PolicySet) Remove(policyID PolicyID) bool { - _, exists := p.policies[policyID] - delete(p.policies, policyID) + _, exists := p.policies.StaticPolicies[policyID] + delete(p.policies.StaticPolicies, policyID) return exists } // Map returns a new PolicyMap instance of the policies in the PolicySet. func (p *PolicySet) Map() PolicyMap { - return maps.Clone(p.policies) + return PolicyMap{ + StaticPolicies: maps.Clone(p.policies.StaticPolicies), + Templates: maps.Clone(p.policies.Templates), + } +} + +func (p PolicySet) Len() int { + return len(p.policies.StaticPolicies) + len(p.policies.Templates) } // MarshalCedar emits a concatenated Cedar representation of a PolicySet. The policy names are stripped, but policies // are emitted in lexicographical order by ID. func (p *PolicySet) MarshalCedar() []byte { - ids := make([]PolicyID, 0, len(p.policies)) - for k := range p.policies { + setSize := p.Len() + + ids := make([]PolicyID, 0, setSize) + for k := range p.policies.StaticPolicies { + ids = append(ids, k) + } + for k := range p.policies.Templates { ids = append(ids, k) } slices.Sort(ids) @@ -86,14 +121,19 @@ func (p *PolicySet) MarshalCedar() []byte { var buf bytes.Buffer i := 0 for _, id := range ids { - policy := p.policies[id] - buf.Write(policy.MarshalCedar()) + if policy, found := p.policies.StaticPolicies[id]; found { + buf.Write(policy.MarshalCedar()) + } else { + template := p.policies.Templates[id] + buf.Write(template.MarshalCedar()) + } - if i < len(p.policies)-1 { + if i < setSize-1 { buf.WriteString("\n\n") } i++ } + return buf.Bytes() } @@ -102,11 +142,16 @@ func (p *PolicySet) MarshalCedar() []byte { // [Cedar documentation]: https://docs.cedarpolicy.com/policies/json-format.html func (p *PolicySet) MarshalJSON() ([]byte, error) { jsonPolicySet := internaljson.PolicySetJSON{ - StaticPolicies: make(internaljson.PolicySet, len(p.policies)), + StaticPolicies: make(internaljson.PolicySet, len(p.policies.StaticPolicies)), + Templates: make(internaljson.TemplateSet, len(p.policies.Templates)), } - for k, v := range p.policies { + for k, v := range p.policies.StaticPolicies { jsonPolicySet.StaticPolicies[string(k)] = (*internaljson.Policy)(v.ast) } + for k, v := range p.policies.Templates { + jsonPolicySet.Templates[string(k)] = (*internaljson.Policy)(v) + } + return json.Marshal(jsonPolicySet) } @@ -119,10 +164,12 @@ func (p *PolicySet) UnmarshalJSON(b []byte) error { return err } *p = PolicySet{ - policies: make(PolicyMap, len(jsonPolicySet.StaticPolicies)), + policies: makePolicyMap(), } + for k, v := range jsonPolicySet.StaticPolicies { - p.policies[PolicyID(k)] = newPolicy((*internalast.Policy)(v)) + p.policies.StaticPolicies[PolicyID(k)] = newPolicy((*internalast.Policy)(v)) } + return nil } diff --git a/policy_set_test.go b/policy_set_test.go index 9397d918..10b5d785 100644 --- a/policy_set_test.go +++ b/policy_set_test.go @@ -111,7 +111,7 @@ forbid ( testutil.OK(t, err) ps := cedar.NewPolicySet() - for i, p := range policies { + for i, p := range policies.StaticPolicies { p.SetFilename("example.cedar") ps.Add(cedar.PolicyID(fmt.Sprintf("policy%d", i)), p) } @@ -128,7 +128,7 @@ func TestPolicyMap(t *testing.T) { ps, err := cedar.NewPolicySetFromBytes("", []byte(`permit (principal, action, resource);`)) testutil.OK(t, err) m := ps.Map() - testutil.Equals(t, len(m), 1) + testutil.Equals(t, len(m.StaticPolicies), 1) } func TestPolicySetJSON(t *testing.T) { @@ -144,7 +144,7 @@ func TestPolicySetJSON(t *testing.T) { var ps cedar.PolicySet err := ps.UnmarshalJSON([]byte(`{"staticPolicies":{"policy0":{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"}}}}`)) testutil.OK(t, err) - testutil.Equals(t, len(ps.Map()), 1) + testutil.Equals(t, len(ps.Map().StaticPolicies), 1) }) t.Run("MarshalOK", func(t *testing.T) { @@ -153,6 +153,6 @@ func TestPolicySetJSON(t *testing.T) { testutil.OK(t, err) out, err := ps.MarshalJSON() testutil.OK(t, err) - testutil.Equals(t, string(out), `{"staticPolicies":{"policy0":{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"}}}}`) + testutil.Equals(t, string(out), `{"staticPolicies":{"policy0":{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"}}},"templates":{}}`) }) } diff --git a/template.go b/template.go new file mode 100644 index 00000000..55cd6aed --- /dev/null +++ b/template.go @@ -0,0 +1,56 @@ +package cedar + +import ( + "bytes" + "github.com/cedar-policy/cedar-go/internal/parser" + "github.com/cedar-policy/cedar-go/types" + internalast "github.com/cedar-policy/cedar-go/x/exp/ast" +) + +type Template parser.Policy + +func (p *Template) MarshalCedar() []byte { + cedarPolicy := (*parser.Policy)(p) + + var buf bytes.Buffer + cedarPolicy.MarshalCedar(&buf) + + return buf.Bytes() +} + +type LinkedPolicy parser.LinkedPolicy + +func LinkTemplate(template Template, templateID string, linkID string, slotEnv map[types.SlotID]types.EntityUID) LinkedPolicy { + t := parser.Template(template) + linkedPolicy := parser.NewLinkedPolicy(&t, templateID, linkID, slotEnv) + + return LinkedPolicy(linkedPolicy) +} + +func (p LinkedPolicy) Render() (*Policy, error) { + pl := parser.LinkedPolicy(p) + + policy, err := pl.Render() + if err != nil { + return nil, err + } + + internalPolicy := internalast.Policy(policy) + + return newPolicy(&internalPolicy), nil +} + +func (p LinkedPolicy) MarshalJSON() ([]byte, error) { + pl := parser.LinkedPolicy(p) + + return pl.MarshalJSON() +} + +func (p PolicySet) StoreLinkedPolicy(lp LinkedPolicy) { + policy, err := lp.Render() + if err != nil { + return + } + + p.Add(PolicyID(lp.LinkID), policy) +} diff --git a/template_test.go b/template_test.go new file mode 100644 index 00000000..6bbe9b53 --- /dev/null +++ b/template_test.go @@ -0,0 +1,124 @@ +package cedar_test + +import ( + "github.com/cedar-policy/cedar-go" + "github.com/cedar-policy/cedar-go/internal/parser" + "github.com/cedar-policy/cedar-go/internal/testutil" + "github.com/cedar-policy/cedar-go/types" + "testing" +) + +func TestLinkTemplateToPolicy(t *testing.T) { + linkTests := []struct { + Name string + TemplateString string + TemplateID string + LinkID string + Env map[types.SlotID]types.EntityUID + Want string + }{ + + { + "principal ScopeTypeEq", + `@id("scope_eq_test") +permit ( + principal == ?principal, + action, + resource +);`, + "scope_eq_test", + "scope_eq_link", + map[types.SlotID]types.EntityUID{"?principal": types.NewEntityUID("User", "bob")}, + `{"annotations":{"id":"scope_eq_test"},"effect":"permit","principal":{"op":"==","entity":{"type":"User","id":"bob"}},"action":{"op":"All"},"resource":{"op":"All"}}`, + }, + + { + "principal ScopeTypeIn", + `@id("scope_in_test") +permit ( + principal in ?principal, + action, + resource +);`, + "scope_in_test", + "scope_in_link", + map[types.SlotID]types.EntityUID{"?principal": types.NewEntityUID("User", "charlie")}, + `{"annotations":{"id":"scope_in_test"},"effect":"permit","principal":{"op":"in","entity":{"type":"User","id":"charlie"}},"action":{"op":"All"},"resource":{"op":"All"}}`, + }, + { + "principal ScopeTypeIsIn", + `@id("scope_isin_test") +permit ( + principal is User in ?principal, + action, + resource +);`, + "scope_isin_test", + "scope_isin_link", + map[types.SlotID]types.EntityUID{"?principal": types.NewEntityUID("User", "dave")}, + `{"annotations":{"id":"scope_isin_test"},"effect":"permit","principal":{"op":"is","entity_type":"User","in":{"entity":{"type":"User","id":"dave"}}},"action":{"op":"All"},"resource":{"op":"All"}}`, + }, + { + "resource ScopeTypeEq", + `@id("resource_scope_eq_test") +permit ( + principal, + action, + resource == ?resource +);`, + "resource_scope_eq_test", + "scope_eq_link", + map[types.SlotID]types.EntityUID{"?resource": types.NewEntityUID("Album", "trip")}, + `{"annotations":{"id":"resource_scope_eq_test"},"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"==","entity":{"type":"Album","id":"trip"}}}`, + }, + { + "resource ScopeTypeIn", + `@id("resource_scope_in_test") +permit ( + principal, + action, + resource in ?resource +);`, + "resource_scope_in_test", + "scope_in_link", + map[types.SlotID]types.EntityUID{"?resource": types.NewEntityUID("Album", "trip")}, + `{"annotations":{"id":"resource_scope_in_test"},"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"in","entity":{"type":"Album","id":"trip"}}}`, + }, + { + "resource ScopeTypeIsIn", + `@id("resource_scope_isin_test") +permit ( + principal, + action, + resource is Album in ?resource +);`, + "resource_scope_isin_test", + "scope_isin_link", + map[types.SlotID]types.EntityUID{"?resource": types.NewEntityUID("Album", "trip")}, + `{"annotations":{"id":"resource_scope_isin_test"},"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"is","entity_type":"Album","in":{"entity":{"type":"Album","id":"trip"}}}}`, + }, + } + + for _, tt := range linkTests { + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + + var templateBody parser.Policy + testutil.OK(t, templateBody.UnmarshalCedar([]byte(tt.TemplateString))) + template := cedar.Template(templateBody) + + linkedPolicy := cedar.LinkTemplate(template, tt.TemplateID, tt.LinkID, tt.Env) + + testutil.Equals(t, linkedPolicy.LinkID, tt.LinkID) + testutil.Equals(t, linkedPolicy.TemplateID, tt.TemplateID) + + policy, err := linkedPolicy.Render() + testutil.OK(t, err) + + pj, err := policy.MarshalJSON() + testutil.OK(t, err) + + testutil.Equals(t, string(pj), tt.Want) + }) + } +} diff --git a/types/entity.go b/types/entity.go index 931dab15..cfbd767a 100644 --- a/types/entity.go +++ b/types/entity.go @@ -6,6 +6,8 @@ import ( "strings" ) +const CedarVariable = EntityType("__cedar::variable") + // An Entity defines the parents and attributes for an EntityUID. type Entity struct { UID EntityUID `json:"uid"` @@ -43,3 +45,14 @@ func (e Entity) MarshalJSON() ([]byte, error) { } return json.Marshal(m) } + +type EntityReference interface { + isEntityReference() +} + +type VariableSlot struct { + ID SlotID `json:"slot"` + Name String `json:"name"` +} + +func (v VariableSlot) isEntityReference() {} diff --git a/types/entity_uid.go b/types/entity_uid.go index 3ed45db1..2a882a9f 100644 --- a/types/entity_uid.go +++ b/types/entity_uid.go @@ -38,6 +38,8 @@ func (a EntityUID) Equal(bi Value) bool { return ok && a == b } +func (a EntityUID) isEntityReference() {} + // String produces a string representation of the EntityUID, e.g. `Type::"id"`. func (v EntityUID) String() string { return string(v.Type) + "::" + strconv.Quote(string(v.ID)) } diff --git a/types/template.go b/types/template.go new file mode 100644 index 00000000..a17e5a87 --- /dev/null +++ b/types/template.go @@ -0,0 +1,12 @@ +package types + +type SlotID string + +const ( + PrincipalSlot SlotID = "?principal" + ResourceSlot SlotID = "?resource" +) + +func (s SlotID) String() string { + return string(s) +} diff --git a/x/exp/ast/policy.go b/x/exp/ast/policy.go index 5578ce39..0427de3a 100644 --- a/x/exp/ast/policy.go +++ b/x/exp/ast/policy.go @@ -36,6 +36,10 @@ type Position struct { Column int // column number, starting at 1 (character count per line) } +type templateContext struct { + slots []types.SlotID +} + type Policy struct { Effect Effect Annotations []AnnotationType // duplicate keys are prevented via the builders @@ -44,6 +48,8 @@ type Policy struct { Resource IsResourceScopeNode Conditions []ConditionType Position Position + + tplCtx templateContext } func newPolicy(effect Effect, annotations []AnnotationType) *Policy { @@ -73,3 +79,29 @@ func (p *Policy) Unless(node Node) *Policy { p.Conditions = append(p.Conditions, ConditionType{Condition: ConditionUnless, Body: node.v}) return p } + +func (p *Policy) AddSlot(slotID types.SlotID) *Policy { + p.tplCtx.slots = append(p.tplCtx.slots, slotID) + return p +} + +func (p *Policy) Slots() []types.SlotID { + return p.tplCtx.slots +} + +func (p *Policy) Clone() Policy { + clonedPolicy := Policy{ + Effect: p.Effect, + Annotations: append([]AnnotationType(nil), p.Annotations...), + Principal: p.Principal, + Action: p.Action, + Resource: p.Resource, + Conditions: append([]ConditionType(nil), p.Conditions...), + Position: p.Position, + tplCtx: templateContext{ + slots: append([]types.SlotID(nil), p.tplCtx.slots...), + }, + } + + return clonedPolicy +} diff --git a/x/exp/ast/scope.go b/x/exp/ast/scope.go index fa685447..8e8d54ab 100644 --- a/x/exp/ast/scope.go +++ b/x/exp/ast/scope.go @@ -10,11 +10,11 @@ func (s Scope) All() ScopeTypeAll { return ScopeTypeAll{} } -func (s Scope) Eq(entity types.EntityUID) ScopeTypeEq { +func (s Scope) Eq(entity types.EntityReference) ScopeTypeEq { return ScopeTypeEq{Entity: entity} } -func (s Scope) In(entity types.EntityUID) ScopeTypeIn { +func (s Scope) In(entity types.EntityReference) ScopeTypeIn { return ScopeTypeIn{Entity: entity} } @@ -26,16 +26,16 @@ func (s Scope) Is(entityType types.EntityType) ScopeTypeIs { return ScopeTypeIs{Type: entityType} } -func (s Scope) IsIn(entityType types.EntityType, entity types.EntityUID) ScopeTypeIsIn { +func (s Scope) IsIn(entityType types.EntityType, entity types.EntityReference) ScopeTypeIsIn { return ScopeTypeIsIn{Type: entityType, Entity: entity} } -func (p *Policy) PrincipalEq(entity types.EntityUID) *Policy { +func (p *Policy) PrincipalEq(entity types.EntityReference) *Policy { p.Principal = Scope{}.Eq(entity) return p } -func (p *Policy) PrincipalIn(entity types.EntityUID) *Policy { +func (p *Policy) PrincipalIn(entity types.EntityReference) *Policy { p.Principal = Scope{}.In(entity) return p } @@ -45,7 +45,7 @@ func (p *Policy) PrincipalIs(entityType types.EntityType) *Policy { return p } -func (p *Policy) PrincipalIsIn(entityType types.EntityType, entity types.EntityUID) *Policy { +func (p *Policy) PrincipalIsIn(entityType types.EntityType, entity types.EntityReference) *Policy { p.Principal = Scope{}.IsIn(entityType, entity) return p } @@ -65,12 +65,12 @@ func (p *Policy) ActionInSet(entities ...types.EntityUID) *Policy { return p } -func (p *Policy) ResourceEq(entity types.EntityUID) *Policy { +func (p *Policy) ResourceEq(entity types.EntityReference) *Policy { p.Resource = Scope{}.Eq(entity) return p } -func (p *Policy) ResourceIn(entity types.EntityUID) *Policy { +func (p *Policy) ResourceIn(entity types.EntityReference) *Policy { p.Resource = Scope{}.In(entity) return p } @@ -80,7 +80,7 @@ func (p *Policy) ResourceIs(entityType types.EntityType) *Policy { return p } -func (p *Policy) ResourceIsIn(entityType types.EntityType, entity types.EntityUID) *Policy { +func (p *Policy) ResourceIsIn(entityType types.EntityType, entity types.EntityReference) *Policy { p.Resource = Scope{}.IsIn(entityType, entity) return p } @@ -127,12 +127,26 @@ type ScopeTypeAll struct { ResourceScopeNode } +func (t ScopeTypeAll) Slot() (slotID types.SlotID, found bool) { + return "", false +} + type ScopeTypeEq struct { ScopeNode PrincipalScopeNode ActionScopeNode ResourceScopeNode - Entity types.EntityUID + Entity types.EntityReference +} + +func (t ScopeTypeEq) Slot() (slotID types.SlotID, found bool) { + switch et := t.Entity.(type) { + case types.VariableSlot: + slotID = et.ID + found = true + } + + return } type ScopeTypeIn struct { @@ -140,7 +154,17 @@ type ScopeTypeIn struct { PrincipalScopeNode ActionScopeNode ResourceScopeNode - Entity types.EntityUID + Entity types.EntityReference +} + +func (t ScopeTypeIn) Slot() (slotID types.SlotID, found bool) { + switch et := t.Entity.(type) { + case types.VariableSlot: + slotID = et.ID + found = true + } + + return } type ScopeTypeInSet struct { @@ -156,10 +180,24 @@ type ScopeTypeIs struct { Type types.EntityType } +func (t ScopeTypeIs) Slot() (slotID types.SlotID, found bool) { + return "", false +} + type ScopeTypeIsIn struct { ScopeNode PrincipalScopeNode ResourceScopeNode Type types.EntityType - Entity types.EntityUID + Entity types.EntityReference +} + +func (t ScopeTypeIsIn) Slot() (slotID types.SlotID, found bool) { + switch et := t.Entity.(type) { + case types.VariableSlot: + slotID = et.ID + found = true + } + + return } diff --git a/x/exp/batch/batch.go b/x/exp/batch/batch.go index 6ce4871e..e3e7cfa5 100644 --- a/x/exp/batch/batch.go +++ b/x/exp/batch/batch.go @@ -140,8 +140,8 @@ func Authorize(ctx context.Context, ps *cedar.PolicySet, entities types.EntityGe } } pm := ps.Map() - be.policies = make(map[types.PolicyID]*ast.Policy, len(pm)) - for k, p := range pm { + be.policies = make(map[types.PolicyID]*ast.Policy, len(pm.StaticPolicies)) + for k, p := range pm.StaticPolicies { be.policies[k] = (*ast.Policy)(p.AST()) } be.callback = cb From a5b5f3128edf54a44092ace2cf2033bd3163f02c Mon Sep 17 00:00:00 2001 From: "Caio Ferreira (aider)" Date: Thu, 3 Apr 2025 11:25:03 -0300 Subject: [PATCH 02/24] feat: add SetFilename method to Template struct Signed-off-by: Caio Ferreira --- template.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/template.go b/template.go index 55cd6aed..b0531aa9 100644 --- a/template.go +++ b/template.go @@ -18,6 +18,11 @@ func (p *Template) MarshalCedar() []byte { return buf.Bytes() } +// SetFilename sets the filename of this template. +func (p *Template) SetFilename(fileName string) { + p.Position.Filename = fileName +} + type LinkedPolicy parser.LinkedPolicy func LinkTemplate(template Template, templateID string, linkID string, slotEnv map[types.SlotID]types.EntityUID) LinkedPolicy { From 00f256269f6c9e3c705f5ad3442706764d765ca5 Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Thu, 3 Apr 2025 11:26:24 -0300 Subject: [PATCH 03/24] feat: set filename in template Signed-off-by: Caio Ferreira --- internal/parser/policy.go | 1 - policy_list.go | 6 +++++- template.go | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/parser/policy.go b/internal/parser/policy.go index a44b7eea..af476bec 100644 --- a/internal/parser/policy.go +++ b/internal/parser/policy.go @@ -8,7 +8,6 @@ func (p *Policy) unwrap() *ast.Policy { return (*ast.Policy)(p) } -//todo: fix type PolicySlice struct { StaticPolicies []*Policy Templates []*Template diff --git a/policy_list.go b/policy_list.go index bd118cad..6cf32bdd 100644 --- a/policy_list.go +++ b/policy_list.go @@ -25,7 +25,11 @@ func NewPolicyListFromBytes(fileName string, document []byte) (PolicyList, error for _, p := range policySlice.StaticPolicies { p.SetFilename(fileName) } - //todo: set template filename + + for _, p := range policySlice.Templates { + p.SetFilename(fileName) + } + return policySlice, nil } diff --git a/template.go b/template.go index b0531aa9..c6d8f822 100644 --- a/template.go +++ b/template.go @@ -51,7 +51,7 @@ func (p LinkedPolicy) MarshalJSON() ([]byte, error) { return pl.MarshalJSON() } -func (p PolicySet) StoreLinkedPolicy(lp LinkedPolicy) { +func (p PolicySet) AddLinkedPolicy(lp LinkedPolicy) { policy, err := lp.Render() if err != nil { return From 1116e25faa33551798d87d49de1d03871f0d5f56 Mon Sep 17 00:00:00 2001 From: "Caio Ferreira (aider)" Date: Thu, 3 Apr 2025 11:29:55 -0300 Subject: [PATCH 04/24] feat: add template management methods to PolicySet Signed-off-by: Caio Ferreira --- template.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/template.go b/template.go index c6d8f822..43d81b95 100644 --- a/template.go +++ b/template.go @@ -51,7 +51,7 @@ func (p LinkedPolicy) MarshalJSON() ([]byte, error) { return pl.MarshalJSON() } -func (p PolicySet) AddLinkedPolicy(lp LinkedPolicy) { +func (p *PolicySet) AddLinkedPolicy(lp LinkedPolicy) { policy, err := lp.Render() if err != nil { return @@ -59,3 +59,25 @@ func (p PolicySet) AddLinkedPolicy(lp LinkedPolicy) { p.Add(PolicyID(lp.LinkID), policy) } + +// GetTemplate returns the Template with the given ID. If a template with the given ID +// does not exist, nil is returned. +func (p PolicySet) GetTemplate(templateID PolicyID) *Template { + return p.policies.Templates[templateID] +} + +// AddTemplate inserts or updates a template with the given ID. Returns true if a template +// with the given ID did not already exist in the set. +func (p *PolicySet) AddTemplate(templateID PolicyID, template *Template) bool { + _, exists := p.policies.Templates[templateID] + p.policies.Templates[templateID] = template + return !exists +} + +// RemoveTemplate removes a template from the PolicySet. Returns true if a template with +// the given ID already existed in the set. +func (p *PolicySet) RemoveTemplate(templateID PolicyID) bool { + _, exists := p.policies.Templates[templateID] + delete(p.policies.Templates, templateID) + return exists +} From b8c96c8330107d5978cc7857036cef3440bb9eda Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Thu, 3 Apr 2025 11:36:34 -0300 Subject: [PATCH 05/24] chore: add godoc to main public types and functions Signed-off-by: Caio Ferreira --- policy_set.go | 2 -- template.go | 35 +++++++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/policy_set.go b/policy_set.go index 0ba3349e..029ce515 100644 --- a/policy_set.go +++ b/policy_set.go @@ -74,8 +74,6 @@ func (p PolicySet) Get(policyID PolicyID) *Policy { return p.policies.StaticPolicies[policyID] } -//todo: add support for templates in Add and Remove - // Add inserts or updates a policy with the given ID. Returns true if a policy // with the given ID did not already exist in the set. func (p *PolicySet) Add(policyID PolicyID, policy *Policy) bool { diff --git a/template.go b/template.go index 43d81b95..c2dd3cd6 100644 --- a/template.go +++ b/template.go @@ -7,8 +7,12 @@ import ( internalast "github.com/cedar-policy/cedar-go/x/exp/ast" ) +// Template represents a Cedar policy template that can be linked with slot values +// to create concrete policies. It's a wrapper around the internal parser.Policy type. type Template parser.Policy +// MarshalCedar serializes the Template into its Cedar language representation. +// Returns the serialized template as a byte slice. func (p *Template) MarshalCedar() []byte { cedarPolicy := (*parser.Policy)(p) @@ -19,12 +23,23 @@ func (p *Template) MarshalCedar() []byte { } // SetFilename sets the filename of this template. +// This is useful for error reporting and debugging purposes. func (p *Template) SetFilename(fileName string) { p.Position.Filename = fileName } +// LinkedPolicy represents a template that has been linked with specific slot values. +// It's a wrapper around the internal parser.LinkedPolicy type. type LinkedPolicy parser.LinkedPolicy +// LinkTemplate creates a LinkedPolicy by binding slot values to a template. +// Parameters: +// - template: The policy template to link +// - templateID: The identifier for the template +// - linkID: The identifier for the resulting linked policy +// - slotEnv: A map of slot IDs to entity UIDs that will be substituted into the template +// +// Returns a LinkedPolicy that can be rendered into a concrete Policy. func LinkTemplate(template Template, templateID string, linkID string, slotEnv map[types.SlotID]types.EntityUID) LinkedPolicy { t := parser.Template(template) linkedPolicy := parser.NewLinkedPolicy(&t, templateID, linkID, slotEnv) @@ -32,6 +47,9 @@ func LinkTemplate(template Template, templateID string, linkID string, slotEnv m return LinkedPolicy(linkedPolicy) } +// Render converts a LinkedPolicy into a concrete Policy by substituting all slot values. +// Returns the rendered Policy and any error that occurred during rendering. +// If rendering fails (e.g., due to missing slot values), an error is returned. func (p LinkedPolicy) Render() (*Policy, error) { pl := parser.LinkedPolicy(p) @@ -45,12 +63,17 @@ func (p LinkedPolicy) Render() (*Policy, error) { return newPolicy(&internalPolicy), nil } +// MarshalJSON serializes the LinkedPolicy into its JSON representation. +// Returns the JSON representation as a byte slice and any error that occurred during marshaling. func (p LinkedPolicy) MarshalJSON() ([]byte, error) { pl := parser.LinkedPolicy(p) return pl.MarshalJSON() } +// AddLinkedPolicy renders a LinkedPolicy and adds the resulting concrete Policy to the PolicySet. +// The policy is added with the LinkID from the LinkedPolicy as its PolicyID. +// If rendering fails, no policy is added to the set. func (p *PolicySet) AddLinkedPolicy(lp LinkedPolicy) { policy, err := lp.Render() if err != nil { @@ -60,22 +83,22 @@ func (p *PolicySet) AddLinkedPolicy(lp LinkedPolicy) { p.Add(PolicyID(lp.LinkID), policy) } -// GetTemplate returns the Template with the given ID. If a template with the given ID -// does not exist, nil is returned. +// GetTemplate returns the Template with the given ID. +// If a template with the given ID does not exist, nil is returned. func (p PolicySet) GetTemplate(templateID PolicyID) *Template { return p.policies.Templates[templateID] } -// AddTemplate inserts or updates a template with the given ID. Returns true if a template -// with the given ID did not already exist in the set. +// AddTemplate inserts or updates a template with the given ID. +// Returns true if a template with the given ID did not already exist in the set. func (p *PolicySet) AddTemplate(templateID PolicyID, template *Template) bool { _, exists := p.policies.Templates[templateID] p.policies.Templates[templateID] = template return !exists } -// RemoveTemplate removes a template from the PolicySet. Returns true if a template with -// the given ID already existed in the set. +// RemoveTemplate removes a template from the PolicySet. +// Returns true if a template with the given ID already existed in the set. func (p *PolicySet) RemoveTemplate(templateID PolicyID) bool { _, exists := p.policies.Templates[templateID] delete(p.policies.Templates, templateID) From b897f966611f211c25cb1d5349cba1169d527176 Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Thu, 3 Apr 2025 11:45:39 -0300 Subject: [PATCH 06/24] test: get, add and remove template functions Signed-off-by: Caio Ferreira --- template_test.go | 63 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/template_test.go b/template_test.go index 6bbe9b53..b79dffc7 100644 --- a/template_test.go +++ b/template_test.go @@ -8,6 +8,69 @@ import ( "testing" ) +func TestPolicySetTemplateManagement(t *testing.T) { + t.Run("template round-trip", func(t *testing.T) { + policySet := cedar.NewPolicySet() + + var templateBody parser.Policy + templateString := `@id("test_template") +permit ( + principal == ?principal, + action, + resource +);` + testutil.OK(t, templateBody.UnmarshalCedar([]byte(templateString))) + template := cedar.Template(templateBody) + + templateID := cedar.PolicyID("test_template_id") + added := policySet.AddTemplate(templateID, &template) + testutil.Equals(t, added, true) + + retrievedTemplate := policySet.GetTemplate(templateID) + testutil.Equals(t, retrievedTemplate != nil, true) + + originalBytes := template.MarshalCedar() + retrievedBytes := retrievedTemplate.MarshalCedar() + testutil.Equals(t, string(retrievedBytes), string(originalBytes)) + + removed := policySet.RemoveTemplate(templateID) + testutil.Equals(t, removed, true) + + retrievedTemplateAfterRemoval := policySet.GetTemplate(templateID) + testutil.Equals(t, retrievedTemplateAfterRemoval, (*cedar.Template)(nil)) + }) + + t.Run("remove non-existent template", func(t *testing.T) { + policySet := cedar.NewPolicySet() + templateID := cedar.PolicyID("non_existent_template") + removed := policySet.RemoveTemplate(templateID) + testutil.Equals(t, removed, false) + }) + + t.Run("add template with existing ID", func(t *testing.T) { + policySet := cedar.NewPolicySet() + templateID := cedar.PolicyID("duplicate_template_id") + + var templateBody parser.Policy + templateString := `@id("test_template") +permit ( + principal, + action, + resource +);` + testutil.OK(t, templateBody.UnmarshalCedar([]byte(templateString))) + template := cedar.Template(templateBody) + + // First add should succeed + isNew := policySet.AddTemplate(templateID, &template) + testutil.Equals(t, isNew, true) + + // Second add with same ID should return false + isNew = policySet.AddTemplate(templateID, &template) + testutil.Equals(t, isNew, false) + }) +} + func TestLinkTemplateToPolicy(t *testing.T) { linkTests := []struct { Name string From 276efe52038146c23882b86101a4715b0b600f8c Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Thu, 3 Apr 2025 13:02:03 -0300 Subject: [PATCH 07/24] feat: fail if both entity and slot are set in json format Signed-off-by: Caio Ferreira --- internal/json/json_test.go | 12 ++++++++++++ internal/json/json_unmarshal.go | 25 +++++++++++-------------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/internal/json/json_test.go b/internal/json/json_test.go index e38900b2..df1de0fe 100644 --- a/internal/json/json_test.go +++ b/internal/json/json_test.go @@ -504,6 +504,18 @@ func TestUnmarshalJSON(t *testing.T) { ast.Permit().ResourceIsIn("Photo", types.VariableSlot{ID: types.ResourceSlot}).AddSlot(types.ResourceSlot), testutil.OK, }, + { + "fail if entity and slot present with equal operator", + `{"effect":"permit","principal":{"op":"==", "slot": "?principal", "entity": {"type": "User", "id": "12UA45"}},"action":{"op":"All"},"resource":{"op":"All"}}`, + nil, + testutil.Error, + }, + { + "fail if entity and slot present with in operator", + `{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"in", "slot": "?resource", "entity": {"type": "User", "id": "12UA45"}}}`, + nil, + testutil.Error, + }, } for _, tt := range tests { diff --git a/internal/json/json_unmarshal.go b/internal/json/json_unmarshal.go index 3da1eb6b..20c665af 100644 --- a/internal/json/json_unmarshal.go +++ b/internal/json/json_unmarshal.go @@ -34,6 +34,10 @@ func slotID(id *string) (types.SlotID, error) { func scopeEntityReference(s *scopeJSON) (types.EntityReference, error) { var ref types.EntityReference + if s.Entity != nil && s.Slot != nil { + return nil, fmt.Errorf("both entity and slot are set") + } + if s.Entity == nil && s.Slot == nil { return nil, fmt.Errorf("entity or slot should be set") } @@ -58,6 +62,10 @@ func scopeEntityReference(s *scopeJSON) (types.EntityReference, error) { func scopeInEntityReference(s *scopeInJSON) (types.EntityReference, error) { var ref types.EntityReference + if s.Entity != nil && s.Slot != nil { + return nil, fmt.Errorf("both entity and slot are set") + } + if s.Entity == nil && s.Slot == nil { return nil, fmt.Errorf("entity or slot should be set") } @@ -84,20 +92,9 @@ func (s *scopeJSON) ToPrincipalResourceNode() (isPrincipalResourceScopeNode, err case "All": return ast.Scope{}.All(), nil case "==": - var ref types.EntityReference - - switch { - case s.Slot != nil: - id, err := slotID(s.Slot) - if err != nil { - return nil, err - } - - ref = types.VariableSlot{ID: id} - case s.Entity != nil: - ref = types.EntityUID(*s.Entity) - default: - return nil, fmt.Errorf("missing entity and slot") + ref, err := scopeEntityReference(s) + if err != nil { + return nil, err } return ast.Scope{}.Eq(ref), nil From cc0491dbbdc71c379a2f8b0bb22294bec028b4ba Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Wed, 14 May 2025 10:55:07 -0300 Subject: [PATCH 08/24] feat: revert public types and functions to work over only static policies Signed-off-by: Caio Ferreira --- policy_list.go | 47 ++---------- policy_set.go | 91 ++++++----------------- template.go | 106 --------------------------- template_test.go | 187 ----------------------------------------------- 4 files changed, 31 insertions(+), 400 deletions(-) delete mode 100644 template.go delete mode 100644 template_test.go diff --git a/policy_list.go b/policy_list.go index 6cf32bdd..cf852d06 100644 --- a/policy_list.go +++ b/policy_list.go @@ -10,26 +10,18 @@ import ( // PolicyList represents a list of un-named Policy's. Cedar documents, unlike the PolicySet form, don't have a means of // naming individual policies. -type PolicyList struct { - StaticPolicies []*Policy - Templates []*Template -} +type PolicyList []*Policy // NewPolicyListFromBytes will create a Policies from the given text document with the given file name used in Position // data. If there is an error parsing the document, it will be returned. func NewPolicyListFromBytes(fileName string, document []byte) (PolicyList, error) { var policySlice PolicyList if err := policySlice.UnmarshalCedar(document); err != nil { - return PolicyList{}, err + return nil, err } - for _, p := range policySlice.StaticPolicies { + for _, p := range policySlice { p.SetFilename(fileName) } - - for _, p := range policySlice.Templates { - p.SetFilename(fileName) - } - return policySlice, nil } @@ -40,47 +32,24 @@ func (p *PolicyList) UnmarshalCedar(b []byte) error { if err := res.UnmarshalCedar(b); err != nil { return fmt.Errorf("parser error: %w", err) } - - staticPolicies := make([]*Policy, 0, len(res.StaticPolicies)) + policySlice := make([]*Policy, 0, len(res.StaticPolicies)) for _, p := range res.StaticPolicies { newPolicy := newPolicy((*internalast.Policy)(p)) - staticPolicies = append(staticPolicies, newPolicy) - } - - templates := make([]*Template, 0, len(res.Templates)) - for _, p := range res.Templates { - t := Template(*p) - templates = append(templates, &t) + policySlice = append(policySlice, newPolicy) } - - p.StaticPolicies = staticPolicies - p.Templates = templates - + *p = policySlice return nil } // MarshalCedar emits a concatenated Cedar representation of the policies. func (p PolicyList) MarshalCedar() []byte { var buf bytes.Buffer - for i, policy := range p.StaticPolicies { + for i, policy := range p { buf.Write(policy.MarshalCedar()) - if i < len(p.StaticPolicies)-1 { + if i < len(p)-1 { buf.WriteString("\n\n") } } - - if len(p.Templates) > 0 { - buf.WriteString("\n\n") - } - - for i, template := range p.Templates { - buf.Write(template.MarshalCedar()) - - if i < len(p.Templates)-1 { - buf.WriteString("\n\n") - } - } - return buf.Bytes() } diff --git a/policy_set.go b/policy_set.go index 764d6ddc..efa430a9 100644 --- a/policy_set.go +++ b/policy_set.go @@ -18,17 +18,7 @@ import ( type PolicyID = types.PolicyID // PolicyMap is a map of policy IDs to policy -type PolicyMap struct { - StaticPolicies map[PolicyID]*Policy - Templates map[PolicyID]*Template -} - -func makePolicyMap() PolicyMap { - return PolicyMap{ - StaticPolicies: make(map[PolicyID]*Policy), - Templates: make(map[PolicyID]*Template), - } -} +type PolicyMap map[PolicyID]*Policy // All returns an iterator over the policy IDs and policies in the PolicyMap. func (p PolicyMap) All() iter.Seq2[PolicyID, *Policy] { @@ -43,7 +33,7 @@ type PolicySet struct { // NewPolicySet creates a new, empty PolicySet func NewPolicySet() *PolicySet { - return &PolicySet{policies: makePolicyMap()} + return &PolicySet{policies: PolicyMap{}} } // NewPolicySetFromBytes will create a PolicySet from the given text document with the given file name used in Position @@ -54,46 +44,35 @@ func NewPolicySet() *PolicySet { func NewPolicySetFromBytes(fileName string, document []byte) (*PolicySet, error) { policySlice, err := NewPolicyListFromBytes(fileName, document) if err != nil { - return nil, err - } - - pm := PolicyMap{ - StaticPolicies: make(map[PolicyID]*Policy, len(policySlice.StaticPolicies)), - Templates: make(map[PolicyID]*Template, len(policySlice.Templates)), + return &PolicySet{}, err } - - for i, p := range policySlice.StaticPolicies { + policyMap := make(PolicyMap, len(policySlice)) + for i, p := range policySlice { policyID := PolicyID(fmt.Sprintf("policy%d", i)) - pm.StaticPolicies[policyID] = p - } - - for i, t := range policySlice.Templates { - policyID := PolicyID(fmt.Sprintf("template%d", i)) - pm.Templates[policyID] = t + policyMap[policyID] = p } - - return &PolicySet{policies: pm}, nil + return &PolicySet{policies: policyMap}, nil } // Get returns the Policy with the given ID. If a policy with the given ID // does not exist, nil is returned. -func (p PolicySet) Get(policyID PolicyID) *Policy { - return p.policies.StaticPolicies[policyID] +func (p *PolicySet) Get(policyID PolicyID) *Policy { + return p.policies[policyID] } // Add inserts or updates a policy with the given ID. Returns true if a policy // with the given ID did not already exist in the set. func (p *PolicySet) Add(policyID PolicyID, policy *Policy) bool { - _, exists := p.policies.StaticPolicies[policyID] - p.policies.StaticPolicies[policyID] = policy + _, exists := p.policies[policyID] + p.policies[policyID] = policy return !exists } // Remove removes a policy from the PolicySet. Returns true if a policy with // the given ID already existed in the set. func (p *PolicySet) Remove(policyID PolicyID) bool { - _, exists := p.policies.StaticPolicies[policyID] - delete(p.policies.StaticPolicies, policyID) + _, exists := p.policies[policyID] + delete(p.policies, policyID) return exists } @@ -101,26 +80,14 @@ func (p *PolicySet) Remove(policyID PolicyID) bool { // // Deprecated: use the iterator returned by All() like so: maps.Collect(ps.All()) func (p *PolicySet) Map() PolicyMap { - return PolicyMap{ - StaticPolicies: maps.Clone(p.policies.StaticPolicies), - Templates: maps.Clone(p.policies.Templates), - } -} - -func (p PolicySet) Len() int { - return len(p.policies.StaticPolicies) + len(p.policies.Templates) + return maps.Clone(p.policies) } // MarshalCedar emits a concatenated Cedar representation of a PolicySet. The policy names are stripped, but policies // are emitted in lexicographical order by ID. func (p *PolicySet) MarshalCedar() []byte { - setSize := p.Len() - - ids := make([]PolicyID, 0, setSize) - for k := range p.policies.StaticPolicies { - ids = append(ids, k) - } - for k := range p.policies.Templates { + ids := make([]PolicyID, 0, len(p.policies)) + for k := range p.policies { ids = append(ids, k) } slices.Sort(ids) @@ -128,19 +95,14 @@ func (p *PolicySet) MarshalCedar() []byte { var buf bytes.Buffer i := 0 for _, id := range ids { - if policy, found := p.policies.StaticPolicies[id]; found { - buf.Write(policy.MarshalCedar()) - } else { - template := p.policies.Templates[id] - buf.Write(template.MarshalCedar()) - } + policy := p.policies[id] + buf.Write(policy.MarshalCedar()) - if i < setSize-1 { + if i < len(p.policies)-1 { buf.WriteString("\n\n") } i++ } - return buf.Bytes() } @@ -149,16 +111,11 @@ func (p *PolicySet) MarshalCedar() []byte { // [Cedar documentation]: https://docs.cedarpolicy.com/policies/json-format.html func (p *PolicySet) MarshalJSON() ([]byte, error) { jsonPolicySet := internaljson.PolicySetJSON{ - StaticPolicies: make(internaljson.PolicySet, len(p.policies.StaticPolicies)), - Templates: make(internaljson.TemplateSet, len(p.policies.Templates)), + StaticPolicies: make(internaljson.PolicySet, len(p.policies)), } - for k, v := range p.policies.StaticPolicies { + for k, v := range p.policies { jsonPolicySet.StaticPolicies[string(k)] = (*internaljson.Policy)(v.ast) } - for k, v := range p.policies.Templates { - jsonPolicySet.Templates[string(k)] = (*internaljson.Policy)(v) - } - return json.Marshal(jsonPolicySet) } @@ -171,13 +128,11 @@ func (p *PolicySet) UnmarshalJSON(b []byte) error { return err } *p = PolicySet{ - policies: makePolicyMap(), + policies: make(PolicyMap, len(jsonPolicySet.StaticPolicies)), } - for k, v := range jsonPolicySet.StaticPolicies { - p.policies.StaticPolicies[PolicyID(k)] = newPolicy((*internalast.Policy)(v)) + p.policies[PolicyID(k)] = newPolicy((*internalast.Policy)(v)) } - return nil } diff --git a/template.go b/template.go deleted file mode 100644 index c2dd3cd6..00000000 --- a/template.go +++ /dev/null @@ -1,106 +0,0 @@ -package cedar - -import ( - "bytes" - "github.com/cedar-policy/cedar-go/internal/parser" - "github.com/cedar-policy/cedar-go/types" - internalast "github.com/cedar-policy/cedar-go/x/exp/ast" -) - -// Template represents a Cedar policy template that can be linked with slot values -// to create concrete policies. It's a wrapper around the internal parser.Policy type. -type Template parser.Policy - -// MarshalCedar serializes the Template into its Cedar language representation. -// Returns the serialized template as a byte slice. -func (p *Template) MarshalCedar() []byte { - cedarPolicy := (*parser.Policy)(p) - - var buf bytes.Buffer - cedarPolicy.MarshalCedar(&buf) - - return buf.Bytes() -} - -// SetFilename sets the filename of this template. -// This is useful for error reporting and debugging purposes. -func (p *Template) SetFilename(fileName string) { - p.Position.Filename = fileName -} - -// LinkedPolicy represents a template that has been linked with specific slot values. -// It's a wrapper around the internal parser.LinkedPolicy type. -type LinkedPolicy parser.LinkedPolicy - -// LinkTemplate creates a LinkedPolicy by binding slot values to a template. -// Parameters: -// - template: The policy template to link -// - templateID: The identifier for the template -// - linkID: The identifier for the resulting linked policy -// - slotEnv: A map of slot IDs to entity UIDs that will be substituted into the template -// -// Returns a LinkedPolicy that can be rendered into a concrete Policy. -func LinkTemplate(template Template, templateID string, linkID string, slotEnv map[types.SlotID]types.EntityUID) LinkedPolicy { - t := parser.Template(template) - linkedPolicy := parser.NewLinkedPolicy(&t, templateID, linkID, slotEnv) - - return LinkedPolicy(linkedPolicy) -} - -// Render converts a LinkedPolicy into a concrete Policy by substituting all slot values. -// Returns the rendered Policy and any error that occurred during rendering. -// If rendering fails (e.g., due to missing slot values), an error is returned. -func (p LinkedPolicy) Render() (*Policy, error) { - pl := parser.LinkedPolicy(p) - - policy, err := pl.Render() - if err != nil { - return nil, err - } - - internalPolicy := internalast.Policy(policy) - - return newPolicy(&internalPolicy), nil -} - -// MarshalJSON serializes the LinkedPolicy into its JSON representation. -// Returns the JSON representation as a byte slice and any error that occurred during marshaling. -func (p LinkedPolicy) MarshalJSON() ([]byte, error) { - pl := parser.LinkedPolicy(p) - - return pl.MarshalJSON() -} - -// AddLinkedPolicy renders a LinkedPolicy and adds the resulting concrete Policy to the PolicySet. -// The policy is added with the LinkID from the LinkedPolicy as its PolicyID. -// If rendering fails, no policy is added to the set. -func (p *PolicySet) AddLinkedPolicy(lp LinkedPolicy) { - policy, err := lp.Render() - if err != nil { - return - } - - p.Add(PolicyID(lp.LinkID), policy) -} - -// GetTemplate returns the Template with the given ID. -// If a template with the given ID does not exist, nil is returned. -func (p PolicySet) GetTemplate(templateID PolicyID) *Template { - return p.policies.Templates[templateID] -} - -// AddTemplate inserts or updates a template with the given ID. -// Returns true if a template with the given ID did not already exist in the set. -func (p *PolicySet) AddTemplate(templateID PolicyID, template *Template) bool { - _, exists := p.policies.Templates[templateID] - p.policies.Templates[templateID] = template - return !exists -} - -// RemoveTemplate removes a template from the PolicySet. -// Returns true if a template with the given ID already existed in the set. -func (p *PolicySet) RemoveTemplate(templateID PolicyID) bool { - _, exists := p.policies.Templates[templateID] - delete(p.policies.Templates, templateID) - return exists -} diff --git a/template_test.go b/template_test.go deleted file mode 100644 index b79dffc7..00000000 --- a/template_test.go +++ /dev/null @@ -1,187 +0,0 @@ -package cedar_test - -import ( - "github.com/cedar-policy/cedar-go" - "github.com/cedar-policy/cedar-go/internal/parser" - "github.com/cedar-policy/cedar-go/internal/testutil" - "github.com/cedar-policy/cedar-go/types" - "testing" -) - -func TestPolicySetTemplateManagement(t *testing.T) { - t.Run("template round-trip", func(t *testing.T) { - policySet := cedar.NewPolicySet() - - var templateBody parser.Policy - templateString := `@id("test_template") -permit ( - principal == ?principal, - action, - resource -);` - testutil.OK(t, templateBody.UnmarshalCedar([]byte(templateString))) - template := cedar.Template(templateBody) - - templateID := cedar.PolicyID("test_template_id") - added := policySet.AddTemplate(templateID, &template) - testutil.Equals(t, added, true) - - retrievedTemplate := policySet.GetTemplate(templateID) - testutil.Equals(t, retrievedTemplate != nil, true) - - originalBytes := template.MarshalCedar() - retrievedBytes := retrievedTemplate.MarshalCedar() - testutil.Equals(t, string(retrievedBytes), string(originalBytes)) - - removed := policySet.RemoveTemplate(templateID) - testutil.Equals(t, removed, true) - - retrievedTemplateAfterRemoval := policySet.GetTemplate(templateID) - testutil.Equals(t, retrievedTemplateAfterRemoval, (*cedar.Template)(nil)) - }) - - t.Run("remove non-existent template", func(t *testing.T) { - policySet := cedar.NewPolicySet() - templateID := cedar.PolicyID("non_existent_template") - removed := policySet.RemoveTemplate(templateID) - testutil.Equals(t, removed, false) - }) - - t.Run("add template with existing ID", func(t *testing.T) { - policySet := cedar.NewPolicySet() - templateID := cedar.PolicyID("duplicate_template_id") - - var templateBody parser.Policy - templateString := `@id("test_template") -permit ( - principal, - action, - resource -);` - testutil.OK(t, templateBody.UnmarshalCedar([]byte(templateString))) - template := cedar.Template(templateBody) - - // First add should succeed - isNew := policySet.AddTemplate(templateID, &template) - testutil.Equals(t, isNew, true) - - // Second add with same ID should return false - isNew = policySet.AddTemplate(templateID, &template) - testutil.Equals(t, isNew, false) - }) -} - -func TestLinkTemplateToPolicy(t *testing.T) { - linkTests := []struct { - Name string - TemplateString string - TemplateID string - LinkID string - Env map[types.SlotID]types.EntityUID - Want string - }{ - - { - "principal ScopeTypeEq", - `@id("scope_eq_test") -permit ( - principal == ?principal, - action, - resource -);`, - "scope_eq_test", - "scope_eq_link", - map[types.SlotID]types.EntityUID{"?principal": types.NewEntityUID("User", "bob")}, - `{"annotations":{"id":"scope_eq_test"},"effect":"permit","principal":{"op":"==","entity":{"type":"User","id":"bob"}},"action":{"op":"All"},"resource":{"op":"All"}}`, - }, - - { - "principal ScopeTypeIn", - `@id("scope_in_test") -permit ( - principal in ?principal, - action, - resource -);`, - "scope_in_test", - "scope_in_link", - map[types.SlotID]types.EntityUID{"?principal": types.NewEntityUID("User", "charlie")}, - `{"annotations":{"id":"scope_in_test"},"effect":"permit","principal":{"op":"in","entity":{"type":"User","id":"charlie"}},"action":{"op":"All"},"resource":{"op":"All"}}`, - }, - { - "principal ScopeTypeIsIn", - `@id("scope_isin_test") -permit ( - principal is User in ?principal, - action, - resource -);`, - "scope_isin_test", - "scope_isin_link", - map[types.SlotID]types.EntityUID{"?principal": types.NewEntityUID("User", "dave")}, - `{"annotations":{"id":"scope_isin_test"},"effect":"permit","principal":{"op":"is","entity_type":"User","in":{"entity":{"type":"User","id":"dave"}}},"action":{"op":"All"},"resource":{"op":"All"}}`, - }, - { - "resource ScopeTypeEq", - `@id("resource_scope_eq_test") -permit ( - principal, - action, - resource == ?resource -);`, - "resource_scope_eq_test", - "scope_eq_link", - map[types.SlotID]types.EntityUID{"?resource": types.NewEntityUID("Album", "trip")}, - `{"annotations":{"id":"resource_scope_eq_test"},"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"==","entity":{"type":"Album","id":"trip"}}}`, - }, - { - "resource ScopeTypeIn", - `@id("resource_scope_in_test") -permit ( - principal, - action, - resource in ?resource -);`, - "resource_scope_in_test", - "scope_in_link", - map[types.SlotID]types.EntityUID{"?resource": types.NewEntityUID("Album", "trip")}, - `{"annotations":{"id":"resource_scope_in_test"},"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"in","entity":{"type":"Album","id":"trip"}}}`, - }, - { - "resource ScopeTypeIsIn", - `@id("resource_scope_isin_test") -permit ( - principal, - action, - resource is Album in ?resource -);`, - "resource_scope_isin_test", - "scope_isin_link", - map[types.SlotID]types.EntityUID{"?resource": types.NewEntityUID("Album", "trip")}, - `{"annotations":{"id":"resource_scope_isin_test"},"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"is","entity_type":"Album","in":{"entity":{"type":"Album","id":"trip"}}}}`, - }, - } - - for _, tt := range linkTests { - t.Run(tt.Name, func(t *testing.T) { - t.Parallel() - - var templateBody parser.Policy - testutil.OK(t, templateBody.UnmarshalCedar([]byte(tt.TemplateString))) - template := cedar.Template(templateBody) - - linkedPolicy := cedar.LinkTemplate(template, tt.TemplateID, tt.LinkID, tt.Env) - - testutil.Equals(t, linkedPolicy.LinkID, tt.LinkID) - testutil.Equals(t, linkedPolicy.TemplateID, tt.TemplateID) - - policy, err := linkedPolicy.Render() - testutil.OK(t, err) - - pj, err := policy.MarshalJSON() - testutil.OK(t, err) - - testutil.Equals(t, string(pj), tt.Want) - }) - } -} From db4a0bee333fa9a71cd1d17ec9095ed4ebb61ca8 Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Wed, 14 May 2025 13:13:56 -0300 Subject: [PATCH 09/24] feat: move template code to experimental package Signed-off-by: Caio Ferreira --- internal/parser/template.go | 23 +- x/exp/templates/authorize.go | 63 +++ x/exp/templates/authorize_test.go | 901 ++++++++++++++++++++++++++++++ x/exp/templates/policy.go | 107 ++++ x/exp/templates/policy_list.go | 85 +++ x/exp/templates/policy_set.go | 202 +++++++ x/exp/templates/template.go | 112 ++++ x/exp/templates/template_test.go | 179 ++++++ 8 files changed, 1671 insertions(+), 1 deletion(-) create mode 100644 x/exp/templates/authorize.go create mode 100644 x/exp/templates/authorize_test.go create mode 100644 x/exp/templates/policy.go create mode 100644 x/exp/templates/policy_list.go create mode 100644 x/exp/templates/policy_set.go create mode 100644 x/exp/templates/template.go create mode 100644 x/exp/templates/template_test.go diff --git a/internal/parser/template.go b/internal/parser/template.go index ffff65be..dc6d0dd9 100644 --- a/internal/parser/template.go +++ b/internal/parser/template.go @@ -3,8 +3,8 @@ package parser import ( "encoding/json" "fmt" - "github.com/cedar-policy/cedar-go/x/exp/ast" "github.com/cedar-policy/cedar-go/types" + "github.com/cedar-policy/cedar-go/x/exp/ast" ) type Template ast.Policy @@ -55,6 +55,27 @@ func (p LinkedPolicy) Render() (Policy, error) { return Policy(*body), nil } +func RenderLinkedPolicy(template *Template, slotEnv map[types.SlotID]types.EntityUID) (Policy, error) { + body := template.ClonePolicy().unwrap() + + if len(body.Slots()) != len(slotEnv) { + return Policy{}, fmt.Errorf("slot env length %d does not match template slot length %d", len(slotEnv), len(body.Slots())) + } + + for _, slot := range body.Slots() { + switch slot { + case types.PrincipalSlot: + body.Principal = linkScope(body.Principal, slotEnv) + case types.ResourceSlot: + body.Resource = linkScope(body.Resource, slotEnv) + default: + return Policy{}, fmt.Errorf("unknown variable %s", slot) + } + } + + return Policy(*body), nil +} + func linkScope[T ast.IsScopeNode](scope T, slotEnv map[types.SlotID]types.EntityUID) T { var linkedScope any = scope diff --git a/x/exp/templates/authorize.go b/x/exp/templates/authorize.go new file mode 100644 index 00000000..db7885c9 --- /dev/null +++ b/x/exp/templates/authorize.go @@ -0,0 +1,63 @@ +package templates + +import ( + "github.com/cedar-policy/cedar-go" + "iter" + + "github.com/cedar-policy/cedar-go/internal/eval" + "github.com/cedar-policy/cedar-go/types" +) + +// PolicyIterator is an interface which abstracts an iterable set of policies. +type PolicyIterator interface { + // All returns an iterator over all the policies in the set + All() iter.Seq2[PolicyID, *Policy] +} + +// Authorize uses the combination of the PolicySet and Entities to determine +// if the given Request to determine Decision and Diagnostic. +func Authorize(policies PolicyIterator, entities types.EntityGetter, req cedar.Request) (cedar.Decision, cedar.Diagnostic) { + if entities == nil { + var zero types.EntityMap + entities = zero + } + env := eval.Env{ + Entities: entities, + Principal: req.Principal, + Action: req.Action, + Resource: req.Resource, + Context: req.Context, + } + var diag cedar.Diagnostic + var forbids []cedar.DiagnosticReason + var permits []cedar.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 policies.All() { + result, err := po.eval.Eval(env) + if err != nil { + diag.Errors = append(diag.Errors, cedar.DiagnosticError{PolicyID: id, Position: po.Position(), Message: err.Error()}) + continue + } + if !result { + continue + } + if po.Effect() == cedar.Forbid { + forbids = append(forbids, cedar.DiagnosticReason{PolicyID: id, Position: po.Position()}) + } else { + permits = append(permits, cedar.DiagnosticReason{PolicyID: id, Position: po.Position()}) + } + } + if len(forbids) > 0 { + diag.Reasons = forbids + return cedar.Deny, diag + } + if len(permits) > 0 { + diag.Reasons = permits + return cedar.Allow, diag + } + return cedar.Deny, diag +} diff --git a/x/exp/templates/authorize_test.go b/x/exp/templates/authorize_test.go new file mode 100644 index 00000000..2f5c8bb4 --- /dev/null +++ b/x/exp/templates/authorize_test.go @@ -0,0 +1,901 @@ +package templates_test + +import ( + "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/templates" +) + +//nolint:revive // due to table test function-length +func TestIsAuthorized(t *testing.T) { + t.Parallel() + cuzco := cedar.NewEntityUID("coder", "cuzco") + dropTable := cedar.NewEntityUID("table", "drop") + tests := []struct { + Name string + Policy string + Entities types.EntityGetter + Principal, Action, Resource cedar.EntityUID + Context cedar.Record + Want cedar.Decision + DiagErr int + ParseErr bool + }{ + { + Name: "simple-permit", + Policy: `permit(principal,action,resource);`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-tags", + Policy: `permit(principal,action,resource) when { principal.hasTag("foo") };`, + Entities: types.EntityMap{ + cuzco: types.Entity{ + Tags: types.NewRecord(cedar.RecordMap{ + "foo": types.String("bar"), + }), + }, + }, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "nil-entity-getter", + Policy: `permit(principal,action,resource);`, + Entities: nil, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "simple-forbid", + Policy: `forbid(principal,action,resource);`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 0, + }, + { + Name: "no-permit", + Policy: `permit(principal,action,resource in asdf::"1234");`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 0, + }, + { + Name: "error-in-policy", + Policy: `permit(principal,action,resource) when { resource in "foo" };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 1, + }, + { + Name: "error-in-policy-continues", + Policy: `permit(principal,action,resource) when { resource in "foo" }; + permit(principal,action,resource); + `, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 1, + }, + { + Name: "permit-requires-context-success", + Policy: `permit(principal,action,resource) when { context.x == 42 };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.NewRecord(cedar.RecordMap{"x": cedar.Long(42)}), + Want: true, + DiagErr: 0, + }, + { + Name: "permit-requires-context-fail", + Policy: `permit(principal,action,resource) when { context.x == 42 };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.NewRecord(cedar.RecordMap{"x": cedar.Long(43)}), + Want: false, + DiagErr: 0, + }, + { + Name: "permit-requires-entities-success", + Policy: `permit(principal,action,resource) when { principal.x == 42 };`, + Entities: cedar.EntityMap{ + cuzco: cedar.Entity{ + UID: cuzco, + Attributes: cedar.NewRecord(cedar.RecordMap{"x": cedar.Long(42)}), + }, + }, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-requires-entities-fail", + Policy: `permit(principal,action,resource) when { principal.x == 42 };`, + Entities: cedar.EntityMap{ + cuzco: cedar.Entity{ + UID: cuzco, + Attributes: cedar.NewRecord(cedar.RecordMap{"x": cedar.Long(43)}), + }, + }, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 0, + }, + { + Name: "permit-requires-entities-parent-success", + Policy: `permit(principal,action,resource) when { principal in parent::"bob" };`, + Entities: cedar.EntityMap{ + cuzco: cedar.Entity{ + UID: cuzco, + Parents: cedar.NewEntityUIDSet(cedar.NewEntityUID("parent", "bob")), + }, + }, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-principal-equals", + Policy: `permit(principal == coder::"cuzco",action,resource);`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-principal-in", + Policy: `permit(principal in team::"osiris",action,resource);`, + Entities: cedar.EntityMap{ + cuzco: cedar.Entity{ + UID: cuzco, + Parents: cedar.NewEntityUIDSet(cedar.NewEntityUID("team", "osiris")), + }, + }, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-action-equals", + Policy: `permit(principal,action == table::"drop",resource);`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-action-in", + Policy: `permit(principal,action in scary::"stuff",resource);`, + Entities: cedar.EntityMap{ + dropTable: cedar.Entity{ + UID: dropTable, + Parents: cedar.NewEntityUIDSet(cedar.NewEntityUID("scary", "stuff")), + }, + }, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-action-in-set", + Policy: `permit(principal,action in [scary::"stuff"],resource);`, + Entities: cedar.EntityMap{ + dropTable: cedar.Entity{ + UID: dropTable, + Parents: cedar.NewEntityUIDSet(cedar.NewEntityUID("scary", "stuff")), + }, + }, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-resource-equals", + Policy: `permit(principal,action,resource == table::"whatever");`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-unless", + Policy: `permit(principal,action,resource) unless { false };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-if", + Policy: `permit(principal,action,resource) when { (if true then true else true) };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-or", + Policy: `permit(principal,action,resource) when { (true || false) };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-and", + Policy: `permit(principal,action,resource) when { (true && true) };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-relations", + Policy: `permit(principal,action,resource) when { (1<2) && (1<=1) && (2>1) && (1>=1) && (1!=2) && (1==1)};`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-relations-in", + Policy: `permit(principal,action,resource) when { principal in principal };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-relations-has", + Policy: `permit(principal,action,resource) when { principal has name };`, + Entities: cedar.EntityMap{ + cuzco: cedar.Entity{ + UID: cuzco, + Attributes: cedar.NewRecord(cedar.RecordMap{"name": cedar.String("bob")}), + }, + }, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-add-sub", + Policy: `permit(principal,action,resource) when { 40+3-1==42 };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-mul", + Policy: `permit(principal,action,resource) when { 6*7==42 };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-negate", + Policy: `permit(principal,action,resource) when { -42==-42 };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-not", + Policy: `permit(principal,action,resource) when { !(1+1==42) };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-set", + Policy: `permit(principal,action,resource) when { [1,2,3].contains(2) };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-record", + Policy: `permit(principal,action,resource) when { {name:"bob"} has name };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-action", + Policy: `permit(principal,action,resource) when { action in action };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-set-contains-ok", + Policy: `permit(principal,action,resource) when { [1,2,3].contains(2) };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-set-contains-error", + Policy: `permit(principal,action,resource) when { [1,2,3].contains(2,3) };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 0, + ParseErr: true, + }, + { + Name: "permit-when-set-containsAll-ok", + Policy: `permit(principal,action,resource) when { [1,2,3].containsAll([2,3]) };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-set-containsAll-error", + Policy: `permit(principal,action,resource) when { [1,2,3].containsAll(2,3) };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 0, + ParseErr: true, + }, + { + Name: "permit-when-set-containsAny-ok", + Policy: `permit(principal,action,resource) when { [1,2,3].containsAny([2,5]) };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-set-containsAny-error", + Policy: `permit(principal,action,resource) when { [1,2,3].containsAny(2,5) };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 0, + ParseErr: true, + }, + { + Name: "permit-when-record-attr", + Policy: `permit(principal,action,resource) when { {name:"bob"}["name"] == "bob" };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-unknown-method", + Policy: `permit(principal,action,resource) when { [1,2,3].shuffle() };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 0, + ParseErr: true, + }, + { + Name: "permit-when-like", + Policy: `permit(principal,action,resource) when { "bananas" like "*nan*" };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-unknown-ext-fun", + Policy: `permit(principal,action,resource) when { fooBar("10") };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 0, + ParseErr: true, + }, + { + Name: "permit-when-decimal", + Policy: `permit(principal,action,resource) when { + decimal("10.0").lessThan(decimal("11.0")) && + decimal("10.0").lessThanOrEqual(decimal("11.0")) && + decimal("10.0").greaterThan(decimal("9.0")) && + decimal("10.0").greaterThanOrEqual(decimal("9.0")) };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-decimal-fun-wrong-arity", + Policy: `permit(principal,action,resource) when { decimal(1, 2) };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 1, + }, + { + Name: "permit-when-datetime", + Policy: `permit(principal,action,resource) when { + datetime("1970-01-01T09:08:07Z") < (datetime("1970-02-01")) && + datetime("1970-01-01T09:08:07Z") <= (datetime("1970-02-01")) && + datetime("1970-01-01T09:08:07Z") > (datetime("1970-01-01")) && + datetime("1970-01-01T09:08:07Z") >= (datetime("1970-01-01")) && + datetime("1970-01-01T09:08:07Z").toDate() == datetime("1970-01-01")};`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-datetime-fun-wrong-arity", + Policy: `permit(principal,action,resource) when { datetime("1970-01-01", "UTC") };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 1, + }, + { + Name: "permit-when-duration", + Policy: `permit(principal,action,resource) when { + duration("9h8m") < (duration("10h")) && + duration("9h8m") <= (duration("10h")) && + duration("9h8m") > (duration("7h")) && + duration("9h8m") >= (duration("7h")) && + duration("1ms").toMilliseconds() == 1 && + duration("1s").toSeconds() == 1 && + duration("1m").toMinutes() == 1 && + duration("1h").toHours() == 1 && + duration("1d").toDays() == 1 && + datetime("1970-01-01").toTime() == duration("0ms") && + datetime("1970-01-01").offset(duration("1ms")).toTime() == duration("1ms") && + datetime("1970-01-01T00:00:00.001Z").durationSince(datetime("1970-01-01")) == duration("1ms")};`, + + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-duration-fun-wrong-arity", + Policy: `permit(principal,action,resource) when { duration("1h", "huh?") };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 1, + }, + { + Name: "permit-when-ip", + Policy: `permit(principal,action,resource) when { + ip("1.2.3.4").isIpv4() && + ip("a:b:c:d::/16").isIpv6() && + ip("::1").isLoopback() && + ip("224.1.2.3").isMulticast() && + ip("127.0.0.1").isInRange(ip("127.0.0.0/16"))};`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "permit-when-ip-fun-wrong-arity", + Policy: `permit(principal,action,resource) when { ip() };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 1, + }, + { + Name: "permit-when-isIpv4-wrong-arity", + Policy: `permit(principal,action,resource) when { ip("1.2.3.4").isIpv4(true) };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 1, + }, + { + Name: "permit-when-isIpv6-wrong-arity", + Policy: `permit(principal,action,resource) when { ip("1.2.3.4").isIpv6(true) };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 1, + }, + { + Name: "permit-when-isLoopback-wrong-arity", + Policy: `permit(principal,action,resource) when { ip("1.2.3.4").isLoopback(true) };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 1, + }, + { + Name: "permit-when-isMulticast-wrong-arity", + Policy: `permit(principal,action,resource) when { ip("1.2.3.4").isMulticast(true) };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 1, + }, + { + Name: "permit-when-isInRange-wrong-arity", + Policy: `permit(principal,action,resource) when { ip("1.2.3.4").isInRange() };`, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: false, + DiagErr: 1, + }, + { + Name: "negative-unary-op", + Policy: `permit(principal,action,resource) when { -context.value > 0 };`, + Entities: cedar.EntityMap{}, + Context: cedar.NewRecord(cedar.RecordMap{"value": cedar.Long(-42)}), + Want: true, + DiagErr: 0, + }, + { + Name: "principal-is", + Policy: `permit(principal is Actor,action,resource);`, + Entities: cedar.EntityMap{}, + Principal: cedar.NewEntityUID("Actor", "cuzco"), + Action: cedar.NewEntityUID("Action", "drop"), + Resource: cedar.NewEntityUID("Resource", "table"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "principal-is-in", + Policy: `permit(principal is Actor in Actor::"cuzco",action,resource);`, + Entities: cedar.EntityMap{}, + Principal: cedar.NewEntityUID("Actor", "cuzco"), + Action: cedar.NewEntityUID("Action", "drop"), + Resource: cedar.NewEntityUID("Resource", "table"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "resource-is", + Policy: `permit(principal,action,resource is Resource);`, + Entities: cedar.EntityMap{}, + Principal: cedar.NewEntityUID("Actor", "cuzco"), + Action: cedar.NewEntityUID("Action", "drop"), + Resource: cedar.NewEntityUID("Resource", "table"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "resource-is-in", + Policy: `permit(principal,action,resource is Resource in Resource::"table");`, + Entities: cedar.EntityMap{}, + Principal: cedar.NewEntityUID("Actor", "cuzco"), + Action: cedar.NewEntityUID("Action", "drop"), + Resource: cedar.NewEntityUID("Resource", "table"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "when-is", + Policy: `permit(principal,action,resource) when { resource is Resource };`, + Entities: cedar.EntityMap{}, + Principal: cedar.NewEntityUID("Actor", "cuzco"), + Action: cedar.NewEntityUID("Action", "drop"), + Resource: cedar.NewEntityUID("Resource", "table"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "when-is-in", + Policy: `permit(principal,action,resource) when { resource is Resource in Resource::"table" };`, + Entities: cedar.EntityMap{}, + Principal: cedar.NewEntityUID("Actor", "cuzco"), + Action: cedar.NewEntityUID("Action", "drop"), + Resource: cedar.NewEntityUID("Resource", "table"), + Context: cedar.Record{}, + Want: true, + DiagErr: 0, + }, + { + Name: "when-is-in", + Policy: `permit(principal,action,resource) when { resource is Resource in Parent::"id" };`, + Entities: cedar.EntityMap{ + cedar.NewEntityUID("Resource", "table"): cedar.Entity{ + UID: cedar.NewEntityUID("Resource", "table"), + Parents: cedar.NewEntityUIDSet(cedar.NewEntityUID("Parent", "id")), + }, + }, + Principal: cedar.NewEntityUID("Actor", "cuzco"), + Action: cedar.NewEntityUID("Action", "drop"), + Resource: cedar.NewEntityUID("Resource", "table"), + Context: cedar.Record{}, + 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: cedar.EntityMap{ + cedar.NewEntityUID("Principal", "1"): cedar.Entity{ + UID: cedar.NewEntityUID("Principal", "1"), + Attributes: cedar.NewRecord(cedar.RecordMap{"bar": cedar.Long(42)}), + }, + }, + Principal: cedar.NewEntityUID("Principal", "1"), + Action: cedar.NewEntityUID("Action", "action"), + Resource: cedar.NewEntityUID("Resource", "resource"), + Context: cedar.NewRecord(cedar.RecordMap{"foo": cedar.Long(43)}), + Want: true, + DiagErr: 0, + }, + { + Name: "isEmpty", + Policy: `permit(principal, action, resource) when { context.foo.isEmpty() && !context.bar.isEmpty() };`, + Entities: cedar.EntityMap{}, + Principal: cedar.NewEntityUID("Principal", "1"), + Action: cedar.NewEntityUID("Action", "action"), + Resource: cedar.NewEntityUID("Resource", "resource"), + Context: cedar.NewRecord(cedar.RecordMap{"foo": cedar.NewSet(), "bar": cedar.NewSet(types.Long(1))}), + Want: true, + DiagErr: 0, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + ps, err := cedar.NewPolicySetFromBytes("policy.cedar", []byte(tt.Policy)) + testutil.Equals(t, err != nil, tt.ParseErr) + ok, diag := ps.IsAuthorized(tt.Entities, cedar.Request{ + Principal: tt.Principal, + Action: tt.Action, + Resource: tt.Resource, + Context: tt.Context, + }) + testutil.Equals(t, len(diag.Errors), tt.DiagErr) + testutil.Equals(t, ok, tt.Want) + + ok, diag = cedar.Authorize(ps, tt.Entities, cedar.Request{ + Principal: tt.Principal, + Action: tt.Action, + Resource: tt.Resource, + Context: tt.Context, + }) + testutil.Equals(t, len(diag.Errors), tt.DiagErr) + testutil.Equals(t, ok, tt.Want) + }) + } +} + +func TestIsAuthorizedFromLinkedPolicies(t *testing.T) { + t.Parallel() + cuzco := cedar.NewEntityUID("coder", "cuzco") + dropTable := cedar.NewEntityUID("table", "drop") + tests := []struct { + Name string + Policy string + LinkEnv map[types.SlotID]types.EntityUID + TemplateID cedar.PolicyID + Entities types.EntityGetter + Principal, Action, Resource cedar.EntityUID + Context cedar.Record + Want cedar.Decision + DiagErr int + ParseErr bool + }{ + { + Name: "simple-permit", + Policy: `permit(principal == ?principal,action,resource);`, + TemplateID: "template0", + LinkEnv: map[types.SlotID]types.EntityUID{"?principal": cuzco}, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: cedar.Allow, + DiagErr: 0, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + ps, err := templates.NewPolicySetFromBytes("policy.cedar", []byte(tt.Policy)) + testutil.Equals(t, err != nil, tt.ParseErr) + + ps.LinkTemplate(tt.TemplateID, "link0", tt.LinkEnv) + + ok, diag := templates.Authorize(ps, tt.Entities, cedar.Request{ + Principal: tt.Principal, + Action: tt.Action, + Resource: tt.Resource, + Context: tt.Context, + }) + testutil.Equals(t, len(diag.Errors), tt.DiagErr) + testutil.Equals(t, ok, tt.Want) + }) + } +} diff --git a/x/exp/templates/policy.go b/x/exp/templates/policy.go new file mode 100644 index 00000000..bb4a1ab4 --- /dev/null +++ b/x/exp/templates/policy.go @@ -0,0 +1,107 @@ +package templates + +import ( + "bytes" + "github.com/cedar-policy/cedar-go" + + "github.com/cedar-policy/cedar-go/ast" + "github.com/cedar-policy/cedar-go/internal/eval" + "github.com/cedar-policy/cedar-go/internal/json" + "github.com/cedar-policy/cedar-go/internal/parser" + internalast "github.com/cedar-policy/cedar-go/x/exp/ast" +) + +// I had to duplicate this file because newPolicy is private +// Otherwise I would have to expose a function to create a Policy from an internal AST + +// A Policy is the parsed form of a single Cedar language policy statement. +type Policy struct { + eval eval.BoolEvaler // determines if a policy matches a request. + ast *internalast.Policy +} + +func newPolicy(astIn *internalast.Policy) *Policy { + return &Policy{eval: eval.Compile(astIn), ast: astIn} +} + +// 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.ast) + 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 = *newPolicy((*internalast.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.ast) + + 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 = *newPolicy((*internalast.Policy)(&cedarPolicy)) + return nil +} + +// NewPolicyFromAST lets you create a new policy statement from a programmatically created AST. +// Do not modify the *ast.Policy after passing it into NewPolicyFromAST. +func NewPolicyFromAST(astIn *ast.Policy) *Policy { + p := newPolicy((*internalast.Policy)(astIn)) + return p +} + +// Annotations retrieves the annotations associated with this policy. +func (p *Policy) Annotations() cedar.Annotations { + res := make(cedar.Annotations, len(p.ast.Annotations)) + for _, e := range p.ast.Annotations { + res[e.Key] = e.Value + } + return res +} + +// Effect retrieves the effect of this policy. +func (p *Policy) Effect() cedar.Effect { + return cedar.Effect(p.ast.Effect) +} + +// Position retrieves the position of this policy. +func (p *Policy) Position() cedar.Position { + return cedar.Position(p.ast.Position) +} + +// SetFilename sets the filename of this policy. +func (p *Policy) SetFilename(fileName string) { + p.ast.Position.Filename = fileName +} + +// AST retrieves the AST of this policy. Do not modify the AST, as the +// compiled policy will no longer be in sync with the AST. +func (p *Policy) AST() *ast.Policy { + return (*ast.Policy)(p.ast) +} diff --git a/x/exp/templates/policy_list.go b/x/exp/templates/policy_list.go new file mode 100644 index 00000000..ead6a62a --- /dev/null +++ b/x/exp/templates/policy_list.go @@ -0,0 +1,85 @@ +package templates + +import ( + "bytes" + "fmt" + "github.com/cedar-policy/cedar-go/internal/parser" + internalast "github.com/cedar-policy/cedar-go/x/exp/ast" +) + +// PolicyList represents a list of un-named Policy's. Cedar documents, unlike the PolicySet form, don't have a means of +// naming individual policies. +type PolicyList struct { + StaticPolicies []*Policy + Templates []*Template +} + +// NewPolicyListFromBytes will create a Policies from the given text document with the given file name used in Position +// data. If there is an error parsing the document, it will be returned. +func NewPolicyListFromBytes(fileName string, document []byte) (PolicyList, error) { + var policySlice PolicyList + if err := policySlice.UnmarshalCedar(document); err != nil { + return PolicyList{}, err + } + for _, p := range policySlice.StaticPolicies { + p.SetFilename(fileName) + } + + for _, p := range policySlice.Templates { + p.SetFilename(fileName) + } + + return policySlice, nil +} + +// UnmarshalCedar parses a concatenation of un-named Cedar policy statements. Names can be assigned to these policies +// when adding them to a PolicySet. +func (p *PolicyList) UnmarshalCedar(b []byte) error { + var res parser.PolicySlice + if err := res.UnmarshalCedar(b); err != nil { + return fmt.Errorf("parser error: %w", err) + } + + staticPolicies := make([]*Policy, 0, len(res.StaticPolicies)) + for _, p := range res.StaticPolicies { + newPolicy := newPolicy((*internalast.Policy)(p)) + staticPolicies = append(staticPolicies, newPolicy) + } + + templates := make([]*Template, 0, len(res.Templates)) + for _, p := range res.Templates { + t := Template(*p) + templates = append(templates, &t) + } + + p.StaticPolicies = staticPolicies + p.Templates = templates + + return nil +} + +// MarshalCedar emits a concatenated Cedar representation of the policies. +func (p PolicyList) MarshalCedar() []byte { + var buf bytes.Buffer + for i, policy := range p.StaticPolicies { + buf.Write(policy.MarshalCedar()) + + if i < len(p.StaticPolicies)-1 { + buf.WriteString("\n\n") + } + } + + if len(p.Templates) > 0 { + buf.WriteString("\n\n") + } + + for i, template := range p.Templates { + buf.Write(template.MarshalCedar()) + + if i < len(p.Templates)-1 { + buf.WriteString("\n\n") + } + } + + return buf.Bytes() +} diff --git a/x/exp/templates/policy_set.go b/x/exp/templates/policy_set.go new file mode 100644 index 00000000..d4dd38bc --- /dev/null +++ b/x/exp/templates/policy_set.go @@ -0,0 +1,202 @@ +// Package templates provides an implementation of the Cedar language authorizer. +package templates + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/cedar-policy/cedar-go" + "github.com/cedar-policy/cedar-go/internal/parser" + "iter" + "maps" + "slices" + + internaljson "github.com/cedar-policy/cedar-go/internal/json" + "github.com/cedar-policy/cedar-go/types" + internalast "github.com/cedar-policy/cedar-go/x/exp/ast" +) + +//revive:disable-next-line:exported +type PolicyID = types.PolicyID + +// PolicyMap is a map of policy IDs to policy +type PolicyMap map[PolicyID]*Policy + +// All returns an iterator over the policy IDs and policies in the PolicyMap. +func (p PolicyMap) All() iter.Seq2[PolicyID, *Policy] { + return maps.All(p) +} + +// PolicySet is a set of named policies against which a request can be authorized. +type PolicySet struct { + // policies are stored internally so we can handle performance, concurrency bookkeeping however we want + policies PolicyMap + + templates map[PolicyID]*Template + links map[PolicyID]*LinkedPolicy2 +} + +// NewPolicySet creates a new, empty PolicySet +func NewPolicySet() *PolicySet { + return &PolicySet{ + policies: PolicyMap{}, + templates: make(map[PolicyID]*Template), + links: make(map[PolicyID]*LinkedPolicy2), + } +} + +// NewPolicySetFromBytes will create a PolicySet from the given text document with the given file name used in Position +// data. If there is an error parsing the document, it will be returned. +// +// NewPolicySetFromBytes assigns default PolicyIDs to the policies contained in fileName in the format "policy" where +// is incremented for each new policy found in the file. +func NewPolicySetFromBytes(fileName string, document []byte) (*PolicySet, error) { + policySlice, err := NewPolicyListFromBytes(fileName, document) + if err != nil { + return &PolicySet{}, err + } + policyMap := make(PolicyMap, len(policySlice.StaticPolicies)) + for i, p := range policySlice.StaticPolicies { + policyID := PolicyID(fmt.Sprintf("policy%d", i)) + policyMap[policyID] = p + } + + templateMap := make(map[PolicyID]*Template, len(policySlice.Templates)) + for i, p := range policySlice.Templates { + policyID := PolicyID(fmt.Sprintf("template%d", i)) + templateMap[policyID] = p + } + + return &PolicySet{policies: policyMap, templates: templateMap, links: make(map[PolicyID]*LinkedPolicy2)}, nil +} + +// Get returns the Policy with the given ID. If a policy with the given ID +// does not exist, nil is returned. +func (p *PolicySet) Get(policyID PolicyID) *Policy { + return p.policies[policyID] +} + +// Add inserts or updates a policy with the given ID. Returns true if a policy +// with the given ID did not already exist in the set. +func (p *PolicySet) Add(policyID PolicyID, policy *Policy) bool { + _, exists := p.policies[policyID] + p.policies[policyID] = policy + return !exists +} + +// Remove removes a policy from the PolicySet. Returns true if a policy with +// the given ID already existed in the set. +func (p *PolicySet) Remove(policyID PolicyID) bool { + _, exists := p.policies[policyID] + delete(p.policies, policyID) + return exists +} + +// Map returns a new PolicyMap instance of the policies in the PolicySet. +// +// Deprecated: use the iterator returned by All() like so: maps.Collect(ps.All()) +func (p *PolicySet) Map() PolicyMap { + return maps.Clone(p.policies) +} + +// MarshalCedar emits a concatenated Cedar representation of a PolicySet. The policy names are stripped, but policies +// are emitted in lexicographical order by ID. +func (p *PolicySet) MarshalCedar() []byte { + ids := make([]PolicyID, 0, len(p.policies)) + for k := range p.policies { + ids = append(ids, k) + } + slices.Sort(ids) + + var buf bytes.Buffer + i := 0 + for _, id := range ids { + policy := p.policies[id] + buf.Write(policy.MarshalCedar()) + + if i < len(p.policies)-1 { + buf.WriteString("\n\n") + } + i++ + } + return buf.Bytes() +} + +// MarshalJSON encodes a PolicySet in the JSON format specified by the [Cedar documentation]. +// +// [Cedar documentation]: https://docs.cedarpolicy.com/policies/json-format.html +func (p *PolicySet) MarshalJSON() ([]byte, error) { + jsonPolicySet := internaljson.PolicySetJSON{ + StaticPolicies: make(internaljson.PolicySet, len(p.policies)), + } + for k, v := range p.policies { + jsonPolicySet.StaticPolicies[string(k)] = (*internaljson.Policy)(v.ast) + } + return json.Marshal(jsonPolicySet) +} + +// UnmarshalJSON parses and compiles a PolicySet in the JSON format specified by the [Cedar documentation]. +// +// [Cedar documentation]: https://docs.cedarpolicy.com/policies/json-format.html +func (p *PolicySet) UnmarshalJSON(b []byte) error { + var jsonPolicySet internaljson.PolicySetJSON + if err := json.Unmarshal(b, &jsonPolicySet); err != nil { + return err + } + *p = PolicySet{ + policies: make(PolicyMap, len(jsonPolicySet.StaticPolicies)), + } + for k, v := range jsonPolicySet.StaticPolicies { + p.policies[PolicyID(k)] = newPolicy((*internalast.Policy)(v)) + } + return nil +} + +// IsAuthorized uses the combination of the PolicySet and Entities to determine +// if the given Request to determine Decision and Diagnostic. +// +// Deprecated: Use the Authorize() function instead +func (p *PolicySet) IsAuthorized(entities types.EntityGetter, req cedar.Request) (cedar.Decision, cedar.Diagnostic) { + return Authorize(p, entities, req) +} + +// All returns an iterator over the (PolicyID, *Policy) tuples in the PolicySet +func (p *PolicySet) All() iter.Seq2[PolicyID, *Policy] { + return func(yield func(PolicyID, *Policy) bool) { + for k, v := range p.policies { + if !yield(k, v) { + break + } + } + + for k, v := range p.links { + // Render links on read to make template changes propagate + policy, err := p.render(*v) + if err != nil { //todo: think how to propagate this error + continue + } + + if !yield(k, policy) { + break + } + } + } +} + +func (p *PolicySet) render(link LinkedPolicy2) (*Policy, error) { + template := p.GetTemplate(link.templateID) + if template == nil { + return nil, fmt.Errorf("no such template %q", link.templateID) + } + + pTemplate := parser.Template(*template) + + policy, err := parser.RenderLinkedPolicy(&pTemplate, link.slotEnv) + if err != nil { + return nil, err + } + + internalPolicy := internalast.Policy(policy) + + return newPolicy(&internalPolicy), nil +} diff --git a/x/exp/templates/template.go b/x/exp/templates/template.go new file mode 100644 index 00000000..e63a966c --- /dev/null +++ b/x/exp/templates/template.go @@ -0,0 +1,112 @@ +package templates + +import ( + "bytes" + "github.com/cedar-policy/cedar-go/internal/parser" + "github.com/cedar-policy/cedar-go/types" + internalast "github.com/cedar-policy/cedar-go/x/exp/ast" +) + +// Template represents a Cedar policy template that can be linked with slot values +// to create concrete policies. It's a wrapper around the internal parser.Policy type. +type Template parser.Policy + +// MarshalCedar serializes the Template into its Cedar language representation. +// Returns the serialized template as a byte slice. +func (p *Template) MarshalCedar() []byte { + cedarPolicy := (*parser.Policy)(p) + + var buf bytes.Buffer + cedarPolicy.MarshalCedar(&buf) + + return buf.Bytes() +} + +// SetFilename sets the filename of this template. +// This is useful for error reporting and debugging purposes. +func (p *Template) SetFilename(fileName string) { + p.Position.Filename = fileName +} + +// LinkedPolicy represents a template that has been linked with specific slot values. +// It's a wrapper around the internal parser.LinkedPolicy type. +type LinkedPolicy parser.LinkedPolicy + +type LinkedPolicy2 struct { + templateID PolicyID + linkID PolicyID + slotEnv map[types.SlotID]types.EntityUID +} + +// LinkTemplate creates a LinkedPolicy by binding slot values to a template. +// Parameters: +// - template: The policy template to link +// - templateID: The identifier for the template +// - linkID: The identifier for the resulting linked policy +// - slotEnv: A map of slot IDs to entity UIDs that will be substituted into the template +// +// Returns a LinkedPolicy that can be rendered into a concrete Policy. +func (p *PolicySet) LinkTemplate(templateID PolicyID, linkID PolicyID, slotEnv map[types.SlotID]types.EntityUID) LinkedPolicy2 { + link := LinkedPolicy2{templateID, linkID, slotEnv} + p.links[linkID] = &link + + return link +} + +// Render converts a LinkedPolicy into a concrete Policy by substituting all slot values. +// Returns the rendered Policy and any error that occurred during rendering. +// If rendering fails (e.g., due to missing slot values), an error is returned. +func (p LinkedPolicy) Render() (*Policy, error) { + pl := parser.LinkedPolicy(p) + + policy, err := pl.Render() + if err != nil { + return nil, err + } + + internalPolicy := internalast.Policy(policy) + + return newPolicy(&internalPolicy), nil +} + +// MarshalJSON serializes the LinkedPolicy into its JSON representation. +// Returns the JSON representation as a byte slice and any error that occurred during marshaling. +func (p LinkedPolicy) MarshalJSON() ([]byte, error) { + pl := parser.LinkedPolicy(p) + + return pl.MarshalJSON() +} + +// AddLinkedPolicy renders a LinkedPolicy and adds the resulting concrete Policy to the PolicySet. +// The policy is added with the LinkID from the LinkedPolicy as its PolicyID. +// If rendering fails, no policy is added to the set. +//func (p *PolicySet) AddLinkedPolicy(lp LinkedPolicy) { +// policy, err := lp.Render() +// if err != nil { +// return +// } +// +// p.Add(PolicyID(lp.LinkID), policy) +//} + +// GetTemplate returns the Template with the given ID. +// If a template with the given ID does not exist, nil is returned. +func (p PolicySet) GetTemplate(templateID PolicyID) *Template { + return p.templates[templateID] +} + +// AddTemplate inserts or updates a template with the given ID. +// Returns true if a template with the given ID did not already exist in the set. +func (p *PolicySet) AddTemplate(templateID PolicyID, template *Template) bool { + _, exists := p.templates[templateID] + p.templates[templateID] = template + return !exists +} + +// RemoveTemplate removes a template from the PolicySet. +// Returns true if a template with the given ID already existed in the set. +func (p *PolicySet) RemoveTemplate(templateID PolicyID) bool { + _, exists := p.templates[templateID] + delete(p.templates, templateID) + return exists +} diff --git a/x/exp/templates/template_test.go b/x/exp/templates/template_test.go new file mode 100644 index 00000000..b5860055 --- /dev/null +++ b/x/exp/templates/template_test.go @@ -0,0 +1,179 @@ +package templates_test + +//func TestPolicySetTemplateManagement(t *testing.T) { +// t.Run("template round-trip", func(t *testing.T) { +// policySet := cedar.NewPolicySet() +// +// var templateBody parser.Policy +// templateString := `@id("test_template") +//permit ( +// principal == ?principal, +// action, +// resource +//);` +// testutil.OK(t, templateBody.UnmarshalCedar([]byte(templateString))) +// template := templates.Template(templateBody) +// +// templateID := cedar.PolicyID("test_template_id") +// added := policySet.AddTemplate(templateID, &template) +// testutil.Equals(t, added, true) +// +// retrievedTemplate := policySet.GetTemplate(templateID) +// testutil.Equals(t, retrievedTemplate != nil, true) +// +// originalBytes := template.MarshalCedar() +// retrievedBytes := retrievedTemplate.MarshalCedar() +// testutil.Equals(t, string(retrievedBytes), string(originalBytes)) +// +// removed := policySet.RemoveTemplate(templateID) +// testutil.Equals(t, removed, true) +// +// retrievedTemplateAfterRemoval := policySet.GetTemplate(templateID) +// testutil.Equals(t, retrievedTemplateAfterRemoval, (*cedar.Template)(nil)) +// }) +// +// t.Run("remove non-existent template", func(t *testing.T) { +// policySet := cedar.NewPolicySet() +// templateID := cedar.PolicyID("non_existent_template") +// removed := policySet.RemoveTemplate(templateID) +// testutil.Equals(t, removed, false) +// }) +// +// t.Run("add template with existing ID", func(t *testing.T) { +// policySet := cedar.NewPolicySet() +// templateID := cedar.PolicyID("duplicate_template_id") +// +// var templateBody parser.Policy +// templateString := `@id("test_template") +//permit ( +// principal, +// action, +// resource +//);` +// testutil.OK(t, templateBody.UnmarshalCedar([]byte(templateString))) +// template := cedar.Template(templateBody) +// +// // First add should succeed +// isNew := policySet.AddTemplate(templateID, &template) +// testutil.Equals(t, isNew, true) +// +// // Second add with same ID should return false +// isNew = policySet.AddTemplate(templateID, &template) +// testutil.Equals(t, isNew, false) +// }) +//} +// +//func TestLinkTemplateToPolicy(t *testing.T) { +// linkTests := []struct { +// Name string +// TemplateString string +// TemplateID string +// LinkID string +// Env map[types.SlotID]types.EntityUID +// Want string +// }{ +// +// { +// "principal ScopeTypeEq", +// `@id("scope_eq_test") +//permit ( +// principal == ?principal, +// action, +// resource +//);`, +// "scope_eq_test", +// "scope_eq_link", +// map[types.SlotID]types.EntityUID{"?principal": types.NewEntityUID("User", "bob")}, +// `{"annotations":{"id":"scope_eq_test"},"effect":"permit","principal":{"op":"==","entity":{"type":"User","id":"bob"}},"action":{"op":"All"},"resource":{"op":"All"}}`, +// }, +// +// { +// "principal ScopeTypeIn", +// `@id("scope_in_test") +//permit ( +// principal in ?principal, +// action, +// resource +//);`, +// "scope_in_test", +// "scope_in_link", +// map[types.SlotID]types.EntityUID{"?principal": types.NewEntityUID("User", "charlie")}, +// `{"annotations":{"id":"scope_in_test"},"effect":"permit","principal":{"op":"in","entity":{"type":"User","id":"charlie"}},"action":{"op":"All"},"resource":{"op":"All"}}`, +// }, +// { +// "principal ScopeTypeIsIn", +// `@id("scope_isin_test") +//permit ( +// principal is User in ?principal, +// action, +// resource +//);`, +// "scope_isin_test", +// "scope_isin_link", +// map[types.SlotID]types.EntityUID{"?principal": types.NewEntityUID("User", "dave")}, +// `{"annotations":{"id":"scope_isin_test"},"effect":"permit","principal":{"op":"is","entity_type":"User","in":{"entity":{"type":"User","id":"dave"}}},"action":{"op":"All"},"resource":{"op":"All"}}`, +// }, +// { +// "resource ScopeTypeEq", +// `@id("resource_scope_eq_test") +//permit ( +// principal, +// action, +// resource == ?resource +//);`, +// "resource_scope_eq_test", +// "scope_eq_link", +// map[types.SlotID]types.EntityUID{"?resource": types.NewEntityUID("Album", "trip")}, +// `{"annotations":{"id":"resource_scope_eq_test"},"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"==","entity":{"type":"Album","id":"trip"}}}`, +// }, +// { +// "resource ScopeTypeIn", +// `@id("resource_scope_in_test") +//permit ( +// principal, +// action, +// resource in ?resource +//);`, +// "resource_scope_in_test", +// "scope_in_link", +// map[types.SlotID]types.EntityUID{"?resource": types.NewEntityUID("Album", "trip")}, +// `{"annotations":{"id":"resource_scope_in_test"},"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"in","entity":{"type":"Album","id":"trip"}}}`, +// }, +// { +// "resource ScopeTypeIsIn", +// `@id("resource_scope_isin_test") +//permit ( +// principal, +// action, +// resource is Album in ?resource +//);`, +// "resource_scope_isin_test", +// "scope_isin_link", +// map[types.SlotID]types.EntityUID{"?resource": types.NewEntityUID("Album", "trip")}, +// `{"annotations":{"id":"resource_scope_isin_test"},"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"is","entity_type":"Album","in":{"entity":{"type":"Album","id":"trip"}}}}`, +// }, +// } +// +// for _, tt := range linkTests { +// t.Run(tt.Name, func(t *testing.T) { +// t.Parallel() +// +// var templateBody parser.Policy +// testutil.OK(t, templateBody.UnmarshalCedar([]byte(tt.TemplateString))) +// template := cedar.Template(templateBody) +// +// linkedPolicy := cedar.LinkTemplate(template, tt.TemplateID, tt.LinkID, tt.Env) +// +// testutil.Equals(t, linkedPolicy.LinkID, tt.LinkID) +// testutil.Equals(t, linkedPolicy.TemplateID, tt.TemplateID) +// +// policy, err := linkedPolicy.Render() +// testutil.OK(t, err) +// +// pj, err := policy.MarshalJSON() +// testutil.OK(t, err) +// +// testutil.Equals(t, string(pj), tt.Want) +// }) +// } +//} From 00dc21b25204f2fc5986d9036f7f0378ad069fc5 Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Wed, 14 May 2025 13:27:24 -0300 Subject: [PATCH 10/24] refact: use public types to avoid duplicating Authorize function Signed-off-by: Caio Ferreira --- x/exp/templates/authorize.go | 63 --- x/exp/templates/authorize_test.go | 842 +----------------------------- x/exp/templates/policy.go | 107 ---- x/exp/templates/policy_list.go | 9 +- x/exp/templates/policy_set.go | 70 +-- x/exp/templates/template.go | 56 +- 6 files changed, 41 insertions(+), 1106 deletions(-) delete mode 100644 x/exp/templates/authorize.go delete mode 100644 x/exp/templates/policy.go diff --git a/x/exp/templates/authorize.go b/x/exp/templates/authorize.go deleted file mode 100644 index db7885c9..00000000 --- a/x/exp/templates/authorize.go +++ /dev/null @@ -1,63 +0,0 @@ -package templates - -import ( - "github.com/cedar-policy/cedar-go" - "iter" - - "github.com/cedar-policy/cedar-go/internal/eval" - "github.com/cedar-policy/cedar-go/types" -) - -// PolicyIterator is an interface which abstracts an iterable set of policies. -type PolicyIterator interface { - // All returns an iterator over all the policies in the set - All() iter.Seq2[PolicyID, *Policy] -} - -// Authorize uses the combination of the PolicySet and Entities to determine -// if the given Request to determine Decision and Diagnostic. -func Authorize(policies PolicyIterator, entities types.EntityGetter, req cedar.Request) (cedar.Decision, cedar.Diagnostic) { - if entities == nil { - var zero types.EntityMap - entities = zero - } - env := eval.Env{ - Entities: entities, - Principal: req.Principal, - Action: req.Action, - Resource: req.Resource, - Context: req.Context, - } - var diag cedar.Diagnostic - var forbids []cedar.DiagnosticReason - var permits []cedar.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 policies.All() { - result, err := po.eval.Eval(env) - if err != nil { - diag.Errors = append(diag.Errors, cedar.DiagnosticError{PolicyID: id, Position: po.Position(), Message: err.Error()}) - continue - } - if !result { - continue - } - if po.Effect() == cedar.Forbid { - forbids = append(forbids, cedar.DiagnosticReason{PolicyID: id, Position: po.Position()}) - } else { - permits = append(permits, cedar.DiagnosticReason{PolicyID: id, Position: po.Position()}) - } - } - if len(forbids) > 0 { - diag.Reasons = forbids - return cedar.Deny, diag - } - if len(permits) > 0 { - diag.Reasons = permits - return cedar.Allow, diag - } - return cedar.Deny, diag -} diff --git a/x/exp/templates/authorize_test.go b/x/exp/templates/authorize_test.go index 2f5c8bb4..da84377e 100644 --- a/x/exp/templates/authorize_test.go +++ b/x/exp/templates/authorize_test.go @@ -9,846 +9,6 @@ import ( "github.com/cedar-policy/cedar-go/x/exp/templates" ) -//nolint:revive // due to table test function-length -func TestIsAuthorized(t *testing.T) { - t.Parallel() - cuzco := cedar.NewEntityUID("coder", "cuzco") - dropTable := cedar.NewEntityUID("table", "drop") - tests := []struct { - Name string - Policy string - Entities types.EntityGetter - Principal, Action, Resource cedar.EntityUID - Context cedar.Record - Want cedar.Decision - DiagErr int - ParseErr bool - }{ - { - Name: "simple-permit", - Policy: `permit(principal,action,resource);`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-tags", - Policy: `permit(principal,action,resource) when { principal.hasTag("foo") };`, - Entities: types.EntityMap{ - cuzco: types.Entity{ - Tags: types.NewRecord(cedar.RecordMap{ - "foo": types.String("bar"), - }), - }, - }, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "nil-entity-getter", - Policy: `permit(principal,action,resource);`, - Entities: nil, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "simple-forbid", - Policy: `forbid(principal,action,resource);`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 0, - }, - { - Name: "no-permit", - Policy: `permit(principal,action,resource in asdf::"1234");`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 0, - }, - { - Name: "error-in-policy", - Policy: `permit(principal,action,resource) when { resource in "foo" };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 1, - }, - { - Name: "error-in-policy-continues", - Policy: `permit(principal,action,resource) when { resource in "foo" }; - permit(principal,action,resource); - `, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 1, - }, - { - Name: "permit-requires-context-success", - Policy: `permit(principal,action,resource) when { context.x == 42 };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.NewRecord(cedar.RecordMap{"x": cedar.Long(42)}), - Want: true, - DiagErr: 0, - }, - { - Name: "permit-requires-context-fail", - Policy: `permit(principal,action,resource) when { context.x == 42 };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.NewRecord(cedar.RecordMap{"x": cedar.Long(43)}), - Want: false, - DiagErr: 0, - }, - { - Name: "permit-requires-entities-success", - Policy: `permit(principal,action,resource) when { principal.x == 42 };`, - Entities: cedar.EntityMap{ - cuzco: cedar.Entity{ - UID: cuzco, - Attributes: cedar.NewRecord(cedar.RecordMap{"x": cedar.Long(42)}), - }, - }, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-requires-entities-fail", - Policy: `permit(principal,action,resource) when { principal.x == 42 };`, - Entities: cedar.EntityMap{ - cuzco: cedar.Entity{ - UID: cuzco, - Attributes: cedar.NewRecord(cedar.RecordMap{"x": cedar.Long(43)}), - }, - }, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 0, - }, - { - Name: "permit-requires-entities-parent-success", - Policy: `permit(principal,action,resource) when { principal in parent::"bob" };`, - Entities: cedar.EntityMap{ - cuzco: cedar.Entity{ - UID: cuzco, - Parents: cedar.NewEntityUIDSet(cedar.NewEntityUID("parent", "bob")), - }, - }, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-principal-equals", - Policy: `permit(principal == coder::"cuzco",action,resource);`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-principal-in", - Policy: `permit(principal in team::"osiris",action,resource);`, - Entities: cedar.EntityMap{ - cuzco: cedar.Entity{ - UID: cuzco, - Parents: cedar.NewEntityUIDSet(cedar.NewEntityUID("team", "osiris")), - }, - }, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-action-equals", - Policy: `permit(principal,action == table::"drop",resource);`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-action-in", - Policy: `permit(principal,action in scary::"stuff",resource);`, - Entities: cedar.EntityMap{ - dropTable: cedar.Entity{ - UID: dropTable, - Parents: cedar.NewEntityUIDSet(cedar.NewEntityUID("scary", "stuff")), - }, - }, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-action-in-set", - Policy: `permit(principal,action in [scary::"stuff"],resource);`, - Entities: cedar.EntityMap{ - dropTable: cedar.Entity{ - UID: dropTable, - Parents: cedar.NewEntityUIDSet(cedar.NewEntityUID("scary", "stuff")), - }, - }, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-resource-equals", - Policy: `permit(principal,action,resource == table::"whatever");`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-unless", - Policy: `permit(principal,action,resource) unless { false };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-if", - Policy: `permit(principal,action,resource) when { (if true then true else true) };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-or", - Policy: `permit(principal,action,resource) when { (true || false) };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-and", - Policy: `permit(principal,action,resource) when { (true && true) };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-relations", - Policy: `permit(principal,action,resource) when { (1<2) && (1<=1) && (2>1) && (1>=1) && (1!=2) && (1==1)};`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-relations-in", - Policy: `permit(principal,action,resource) when { principal in principal };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-relations-has", - Policy: `permit(principal,action,resource) when { principal has name };`, - Entities: cedar.EntityMap{ - cuzco: cedar.Entity{ - UID: cuzco, - Attributes: cedar.NewRecord(cedar.RecordMap{"name": cedar.String("bob")}), - }, - }, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-add-sub", - Policy: `permit(principal,action,resource) when { 40+3-1==42 };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-mul", - Policy: `permit(principal,action,resource) when { 6*7==42 };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-negate", - Policy: `permit(principal,action,resource) when { -42==-42 };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-not", - Policy: `permit(principal,action,resource) when { !(1+1==42) };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-set", - Policy: `permit(principal,action,resource) when { [1,2,3].contains(2) };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-record", - Policy: `permit(principal,action,resource) when { {name:"bob"} has name };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-action", - Policy: `permit(principal,action,resource) when { action in action };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-set-contains-ok", - Policy: `permit(principal,action,resource) when { [1,2,3].contains(2) };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-set-contains-error", - Policy: `permit(principal,action,resource) when { [1,2,3].contains(2,3) };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 0, - ParseErr: true, - }, - { - Name: "permit-when-set-containsAll-ok", - Policy: `permit(principal,action,resource) when { [1,2,3].containsAll([2,3]) };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-set-containsAll-error", - Policy: `permit(principal,action,resource) when { [1,2,3].containsAll(2,3) };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 0, - ParseErr: true, - }, - { - Name: "permit-when-set-containsAny-ok", - Policy: `permit(principal,action,resource) when { [1,2,3].containsAny([2,5]) };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-set-containsAny-error", - Policy: `permit(principal,action,resource) when { [1,2,3].containsAny(2,5) };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 0, - ParseErr: true, - }, - { - Name: "permit-when-record-attr", - Policy: `permit(principal,action,resource) when { {name:"bob"}["name"] == "bob" };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-unknown-method", - Policy: `permit(principal,action,resource) when { [1,2,3].shuffle() };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 0, - ParseErr: true, - }, - { - Name: "permit-when-like", - Policy: `permit(principal,action,resource) when { "bananas" like "*nan*" };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-unknown-ext-fun", - Policy: `permit(principal,action,resource) when { fooBar("10") };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 0, - ParseErr: true, - }, - { - Name: "permit-when-decimal", - Policy: `permit(principal,action,resource) when { - decimal("10.0").lessThan(decimal("11.0")) && - decimal("10.0").lessThanOrEqual(decimal("11.0")) && - decimal("10.0").greaterThan(decimal("9.0")) && - decimal("10.0").greaterThanOrEqual(decimal("9.0")) };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-decimal-fun-wrong-arity", - Policy: `permit(principal,action,resource) when { decimal(1, 2) };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 1, - }, - { - Name: "permit-when-datetime", - Policy: `permit(principal,action,resource) when { - datetime("1970-01-01T09:08:07Z") < (datetime("1970-02-01")) && - datetime("1970-01-01T09:08:07Z") <= (datetime("1970-02-01")) && - datetime("1970-01-01T09:08:07Z") > (datetime("1970-01-01")) && - datetime("1970-01-01T09:08:07Z") >= (datetime("1970-01-01")) && - datetime("1970-01-01T09:08:07Z").toDate() == datetime("1970-01-01")};`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-datetime-fun-wrong-arity", - Policy: `permit(principal,action,resource) when { datetime("1970-01-01", "UTC") };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 1, - }, - { - Name: "permit-when-duration", - Policy: `permit(principal,action,resource) when { - duration("9h8m") < (duration("10h")) && - duration("9h8m") <= (duration("10h")) && - duration("9h8m") > (duration("7h")) && - duration("9h8m") >= (duration("7h")) && - duration("1ms").toMilliseconds() == 1 && - duration("1s").toSeconds() == 1 && - duration("1m").toMinutes() == 1 && - duration("1h").toHours() == 1 && - duration("1d").toDays() == 1 && - datetime("1970-01-01").toTime() == duration("0ms") && - datetime("1970-01-01").offset(duration("1ms")).toTime() == duration("1ms") && - datetime("1970-01-01T00:00:00.001Z").durationSince(datetime("1970-01-01")) == duration("1ms")};`, - - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-duration-fun-wrong-arity", - Policy: `permit(principal,action,resource) when { duration("1h", "huh?") };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 1, - }, - { - Name: "permit-when-ip", - Policy: `permit(principal,action,resource) when { - ip("1.2.3.4").isIpv4() && - ip("a:b:c:d::/16").isIpv6() && - ip("::1").isLoopback() && - ip("224.1.2.3").isMulticast() && - ip("127.0.0.1").isInRange(ip("127.0.0.0/16"))};`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "permit-when-ip-fun-wrong-arity", - Policy: `permit(principal,action,resource) when { ip() };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 1, - }, - { - Name: "permit-when-isIpv4-wrong-arity", - Policy: `permit(principal,action,resource) when { ip("1.2.3.4").isIpv4(true) };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 1, - }, - { - Name: "permit-when-isIpv6-wrong-arity", - Policy: `permit(principal,action,resource) when { ip("1.2.3.4").isIpv6(true) };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 1, - }, - { - Name: "permit-when-isLoopback-wrong-arity", - Policy: `permit(principal,action,resource) when { ip("1.2.3.4").isLoopback(true) };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 1, - }, - { - Name: "permit-when-isMulticast-wrong-arity", - Policy: `permit(principal,action,resource) when { ip("1.2.3.4").isMulticast(true) };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 1, - }, - { - Name: "permit-when-isInRange-wrong-arity", - Policy: `permit(principal,action,resource) when { ip("1.2.3.4").isInRange() };`, - Entities: cedar.EntityMap{}, - Principal: cuzco, - Action: dropTable, - Resource: cedar.NewEntityUID("table", "whatever"), - Context: cedar.Record{}, - Want: false, - DiagErr: 1, - }, - { - Name: "negative-unary-op", - Policy: `permit(principal,action,resource) when { -context.value > 0 };`, - Entities: cedar.EntityMap{}, - Context: cedar.NewRecord(cedar.RecordMap{"value": cedar.Long(-42)}), - Want: true, - DiagErr: 0, - }, - { - Name: "principal-is", - Policy: `permit(principal is Actor,action,resource);`, - Entities: cedar.EntityMap{}, - Principal: cedar.NewEntityUID("Actor", "cuzco"), - Action: cedar.NewEntityUID("Action", "drop"), - Resource: cedar.NewEntityUID("Resource", "table"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "principal-is-in", - Policy: `permit(principal is Actor in Actor::"cuzco",action,resource);`, - Entities: cedar.EntityMap{}, - Principal: cedar.NewEntityUID("Actor", "cuzco"), - Action: cedar.NewEntityUID("Action", "drop"), - Resource: cedar.NewEntityUID("Resource", "table"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "resource-is", - Policy: `permit(principal,action,resource is Resource);`, - Entities: cedar.EntityMap{}, - Principal: cedar.NewEntityUID("Actor", "cuzco"), - Action: cedar.NewEntityUID("Action", "drop"), - Resource: cedar.NewEntityUID("Resource", "table"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "resource-is-in", - Policy: `permit(principal,action,resource is Resource in Resource::"table");`, - Entities: cedar.EntityMap{}, - Principal: cedar.NewEntityUID("Actor", "cuzco"), - Action: cedar.NewEntityUID("Action", "drop"), - Resource: cedar.NewEntityUID("Resource", "table"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "when-is", - Policy: `permit(principal,action,resource) when { resource is Resource };`, - Entities: cedar.EntityMap{}, - Principal: cedar.NewEntityUID("Actor", "cuzco"), - Action: cedar.NewEntityUID("Action", "drop"), - Resource: cedar.NewEntityUID("Resource", "table"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "when-is-in", - Policy: `permit(principal,action,resource) when { resource is Resource in Resource::"table" };`, - Entities: cedar.EntityMap{}, - Principal: cedar.NewEntityUID("Actor", "cuzco"), - Action: cedar.NewEntityUID("Action", "drop"), - Resource: cedar.NewEntityUID("Resource", "table"), - Context: cedar.Record{}, - Want: true, - DiagErr: 0, - }, - { - Name: "when-is-in", - Policy: `permit(principal,action,resource) when { resource is Resource in Parent::"id" };`, - Entities: cedar.EntityMap{ - cedar.NewEntityUID("Resource", "table"): cedar.Entity{ - UID: cedar.NewEntityUID("Resource", "table"), - Parents: cedar.NewEntityUIDSet(cedar.NewEntityUID("Parent", "id")), - }, - }, - Principal: cedar.NewEntityUID("Actor", "cuzco"), - Action: cedar.NewEntityUID("Action", "drop"), - Resource: cedar.NewEntityUID("Resource", "table"), - Context: cedar.Record{}, - 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: cedar.EntityMap{ - cedar.NewEntityUID("Principal", "1"): cedar.Entity{ - UID: cedar.NewEntityUID("Principal", "1"), - Attributes: cedar.NewRecord(cedar.RecordMap{"bar": cedar.Long(42)}), - }, - }, - Principal: cedar.NewEntityUID("Principal", "1"), - Action: cedar.NewEntityUID("Action", "action"), - Resource: cedar.NewEntityUID("Resource", "resource"), - Context: cedar.NewRecord(cedar.RecordMap{"foo": cedar.Long(43)}), - Want: true, - DiagErr: 0, - }, - { - Name: "isEmpty", - Policy: `permit(principal, action, resource) when { context.foo.isEmpty() && !context.bar.isEmpty() };`, - Entities: cedar.EntityMap{}, - Principal: cedar.NewEntityUID("Principal", "1"), - Action: cedar.NewEntityUID("Action", "action"), - Resource: cedar.NewEntityUID("Resource", "resource"), - Context: cedar.NewRecord(cedar.RecordMap{"foo": cedar.NewSet(), "bar": cedar.NewSet(types.Long(1))}), - Want: true, - DiagErr: 0, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.Name, func(t *testing.T) { - t.Parallel() - ps, err := cedar.NewPolicySetFromBytes("policy.cedar", []byte(tt.Policy)) - testutil.Equals(t, err != nil, tt.ParseErr) - ok, diag := ps.IsAuthorized(tt.Entities, cedar.Request{ - Principal: tt.Principal, - Action: tt.Action, - Resource: tt.Resource, - Context: tt.Context, - }) - testutil.Equals(t, len(diag.Errors), tt.DiagErr) - testutil.Equals(t, ok, tt.Want) - - ok, diag = cedar.Authorize(ps, tt.Entities, cedar.Request{ - Principal: tt.Principal, - Action: tt.Action, - Resource: tt.Resource, - Context: tt.Context, - }) - testutil.Equals(t, len(diag.Errors), tt.DiagErr) - testutil.Equals(t, ok, tt.Want) - }) - } -} - func TestIsAuthorizedFromLinkedPolicies(t *testing.T) { t.Parallel() cuzco := cedar.NewEntityUID("coder", "cuzco") @@ -888,7 +48,7 @@ func TestIsAuthorizedFromLinkedPolicies(t *testing.T) { ps.LinkTemplate(tt.TemplateID, "link0", tt.LinkEnv) - ok, diag := templates.Authorize(ps, tt.Entities, cedar.Request{ + ok, diag := cedar.Authorize(ps, tt.Entities, cedar.Request{ Principal: tt.Principal, Action: tt.Action, Resource: tt.Resource, diff --git a/x/exp/templates/policy.go b/x/exp/templates/policy.go deleted file mode 100644 index bb4a1ab4..00000000 --- a/x/exp/templates/policy.go +++ /dev/null @@ -1,107 +0,0 @@ -package templates - -import ( - "bytes" - "github.com/cedar-policy/cedar-go" - - "github.com/cedar-policy/cedar-go/ast" - "github.com/cedar-policy/cedar-go/internal/eval" - "github.com/cedar-policy/cedar-go/internal/json" - "github.com/cedar-policy/cedar-go/internal/parser" - internalast "github.com/cedar-policy/cedar-go/x/exp/ast" -) - -// I had to duplicate this file because newPolicy is private -// Otherwise I would have to expose a function to create a Policy from an internal AST - -// A Policy is the parsed form of a single Cedar language policy statement. -type Policy struct { - eval eval.BoolEvaler // determines if a policy matches a request. - ast *internalast.Policy -} - -func newPolicy(astIn *internalast.Policy) *Policy { - return &Policy{eval: eval.Compile(astIn), ast: astIn} -} - -// 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.ast) - 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 = *newPolicy((*internalast.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.ast) - - 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 = *newPolicy((*internalast.Policy)(&cedarPolicy)) - return nil -} - -// NewPolicyFromAST lets you create a new policy statement from a programmatically created AST. -// Do not modify the *ast.Policy after passing it into NewPolicyFromAST. -func NewPolicyFromAST(astIn *ast.Policy) *Policy { - p := newPolicy((*internalast.Policy)(astIn)) - return p -} - -// Annotations retrieves the annotations associated with this policy. -func (p *Policy) Annotations() cedar.Annotations { - res := make(cedar.Annotations, len(p.ast.Annotations)) - for _, e := range p.ast.Annotations { - res[e.Key] = e.Value - } - return res -} - -// Effect retrieves the effect of this policy. -func (p *Policy) Effect() cedar.Effect { - return cedar.Effect(p.ast.Effect) -} - -// Position retrieves the position of this policy. -func (p *Policy) Position() cedar.Position { - return cedar.Position(p.ast.Position) -} - -// SetFilename sets the filename of this policy. -func (p *Policy) SetFilename(fileName string) { - p.ast.Position.Filename = fileName -} - -// AST retrieves the AST of this policy. Do not modify the AST, as the -// compiled policy will no longer be in sync with the AST. -func (p *Policy) AST() *ast.Policy { - return (*ast.Policy)(p.ast) -} diff --git a/x/exp/templates/policy_list.go b/x/exp/templates/policy_list.go index ead6a62a..3dee9b90 100644 --- a/x/exp/templates/policy_list.go +++ b/x/exp/templates/policy_list.go @@ -3,14 +3,15 @@ package templates import ( "bytes" "fmt" + "github.com/cedar-policy/cedar-go" + "github.com/cedar-policy/cedar-go/ast" "github.com/cedar-policy/cedar-go/internal/parser" - internalast "github.com/cedar-policy/cedar-go/x/exp/ast" ) // PolicyList represents a list of un-named Policy's. Cedar documents, unlike the PolicySet form, don't have a means of // naming individual policies. type PolicyList struct { - StaticPolicies []*Policy + StaticPolicies []*cedar.Policy Templates []*Template } @@ -40,9 +41,9 @@ func (p *PolicyList) UnmarshalCedar(b []byte) error { return fmt.Errorf("parser error: %w", err) } - staticPolicies := make([]*Policy, 0, len(res.StaticPolicies)) + staticPolicies := make([]*cedar.Policy, 0, len(res.StaticPolicies)) for _, p := range res.StaticPolicies { - newPolicy := newPolicy((*internalast.Policy)(p)) + newPolicy := cedar.NewPolicyFromAST((*ast.Policy)(p)) staticPolicies = append(staticPolicies, newPolicy) } diff --git a/x/exp/templates/policy_set.go b/x/exp/templates/policy_set.go index d4dd38bc..2fa0ed67 100644 --- a/x/exp/templates/policy_set.go +++ b/x/exp/templates/policy_set.go @@ -6,42 +6,30 @@ import ( "encoding/json" "fmt" "github.com/cedar-policy/cedar-go" + "github.com/cedar-policy/cedar-go/ast" "github.com/cedar-policy/cedar-go/internal/parser" "iter" "maps" "slices" internaljson "github.com/cedar-policy/cedar-go/internal/json" - "github.com/cedar-policy/cedar-go/types" - internalast "github.com/cedar-policy/cedar-go/x/exp/ast" ) -//revive:disable-next-line:exported -type PolicyID = types.PolicyID - -// PolicyMap is a map of policy IDs to policy -type PolicyMap map[PolicyID]*Policy - -// All returns an iterator over the policy IDs and policies in the PolicyMap. -func (p PolicyMap) All() iter.Seq2[PolicyID, *Policy] { - return maps.All(p) -} - // PolicySet is a set of named policies against which a request can be authorized. type PolicySet struct { // policies are stored internally so we can handle performance, concurrency bookkeeping however we want - policies PolicyMap + policies cedar.PolicyMap - templates map[PolicyID]*Template - links map[PolicyID]*LinkedPolicy2 + templates map[cedar.PolicyID]*Template + links map[cedar.PolicyID]*LinkedPolicy } // NewPolicySet creates a new, empty PolicySet func NewPolicySet() *PolicySet { return &PolicySet{ - policies: PolicyMap{}, - templates: make(map[PolicyID]*Template), - links: make(map[PolicyID]*LinkedPolicy2), + policies: cedar.PolicyMap{}, + templates: make(map[cedar.PolicyID]*Template), + links: make(map[cedar.PolicyID]*LinkedPolicy), } } @@ -55,30 +43,30 @@ func NewPolicySetFromBytes(fileName string, document []byte) (*PolicySet, error) if err != nil { return &PolicySet{}, err } - policyMap := make(PolicyMap, len(policySlice.StaticPolicies)) + policyMap := make(cedar.PolicyMap, len(policySlice.StaticPolicies)) for i, p := range policySlice.StaticPolicies { - policyID := PolicyID(fmt.Sprintf("policy%d", i)) + policyID := cedar.PolicyID(fmt.Sprintf("policy%d", i)) policyMap[policyID] = p } - templateMap := make(map[PolicyID]*Template, len(policySlice.Templates)) + templateMap := make(map[cedar.PolicyID]*Template, len(policySlice.Templates)) for i, p := range policySlice.Templates { - policyID := PolicyID(fmt.Sprintf("template%d", i)) + policyID := cedar.PolicyID(fmt.Sprintf("template%d", i)) templateMap[policyID] = p } - return &PolicySet{policies: policyMap, templates: templateMap, links: make(map[PolicyID]*LinkedPolicy2)}, nil + return &PolicySet{policies: policyMap, templates: templateMap, links: make(map[cedar.PolicyID]*LinkedPolicy)}, nil } // Get returns the Policy with the given ID. If a policy with the given ID // does not exist, nil is returned. -func (p *PolicySet) Get(policyID PolicyID) *Policy { +func (p *PolicySet) Get(policyID cedar.PolicyID) *cedar.Policy { return p.policies[policyID] } // Add inserts or updates a policy with the given ID. Returns true if a policy // with the given ID did not already exist in the set. -func (p *PolicySet) Add(policyID PolicyID, policy *Policy) bool { +func (p *PolicySet) Add(policyID cedar.PolicyID, policy *cedar.Policy) bool { _, exists := p.policies[policyID] p.policies[policyID] = policy return !exists @@ -86,7 +74,7 @@ func (p *PolicySet) Add(policyID PolicyID, policy *Policy) bool { // Remove removes a policy from the PolicySet. Returns true if a policy with // the given ID already existed in the set. -func (p *PolicySet) Remove(policyID PolicyID) bool { +func (p *PolicySet) Remove(policyID cedar.PolicyID) bool { _, exists := p.policies[policyID] delete(p.policies, policyID) return exists @@ -95,14 +83,14 @@ func (p *PolicySet) Remove(policyID PolicyID) bool { // Map returns a new PolicyMap instance of the policies in the PolicySet. // // Deprecated: use the iterator returned by All() like so: maps.Collect(ps.All()) -func (p *PolicySet) Map() PolicyMap { +func (p *PolicySet) Map() cedar.PolicyMap { return maps.Clone(p.policies) } // MarshalCedar emits a concatenated Cedar representation of a PolicySet. The policy names are stripped, but policies // are emitted in lexicographical order by ID. func (p *PolicySet) MarshalCedar() []byte { - ids := make([]PolicyID, 0, len(p.policies)) + ids := make([]cedar.PolicyID, 0, len(p.policies)) for k := range p.policies { ids = append(ids, k) } @@ -130,7 +118,7 @@ func (p *PolicySet) MarshalJSON() ([]byte, error) { StaticPolicies: make(internaljson.PolicySet, len(p.policies)), } for k, v := range p.policies { - jsonPolicySet.StaticPolicies[string(k)] = (*internaljson.Policy)(v.ast) + jsonPolicySet.StaticPolicies[string(k)] = (*internaljson.Policy)(v.AST()) } return json.Marshal(jsonPolicySet) } @@ -144,25 +132,17 @@ func (p *PolicySet) UnmarshalJSON(b []byte) error { return err } *p = PolicySet{ - policies: make(PolicyMap, len(jsonPolicySet.StaticPolicies)), + policies: make(cedar.PolicyMap, len(jsonPolicySet.StaticPolicies)), } for k, v := range jsonPolicySet.StaticPolicies { - p.policies[PolicyID(k)] = newPolicy((*internalast.Policy)(v)) + p.policies[cedar.PolicyID(k)] = cedar.NewPolicyFromAST((*ast.Policy)(v)) } return nil } -// IsAuthorized uses the combination of the PolicySet and Entities to determine -// if the given Request to determine Decision and Diagnostic. -// -// Deprecated: Use the Authorize() function instead -func (p *PolicySet) IsAuthorized(entities types.EntityGetter, req cedar.Request) (cedar.Decision, cedar.Diagnostic) { - return Authorize(p, entities, req) -} - // All returns an iterator over the (PolicyID, *Policy) tuples in the PolicySet -func (p *PolicySet) All() iter.Seq2[PolicyID, *Policy] { - return func(yield func(PolicyID, *Policy) bool) { +func (p *PolicySet) All() iter.Seq2[cedar.PolicyID, *cedar.Policy] { + return func(yield func(cedar.PolicyID, *cedar.Policy) bool) { for k, v := range p.policies { if !yield(k, v) { break @@ -183,7 +163,7 @@ func (p *PolicySet) All() iter.Seq2[PolicyID, *Policy] { } } -func (p *PolicySet) render(link LinkedPolicy2) (*Policy, error) { +func (p *PolicySet) render(link LinkedPolicy) (*cedar.Policy, error) { template := p.GetTemplate(link.templateID) if template == nil { return nil, fmt.Errorf("no such template %q", link.templateID) @@ -196,7 +176,7 @@ func (p *PolicySet) render(link LinkedPolicy2) (*Policy, error) { return nil, err } - internalPolicy := internalast.Policy(policy) + astPolicy := ast.Policy(policy) - return newPolicy(&internalPolicy), nil + return cedar.NewPolicyFromAST(&astPolicy), nil } diff --git a/x/exp/templates/template.go b/x/exp/templates/template.go index e63a966c..72c929ad 100644 --- a/x/exp/templates/template.go +++ b/x/exp/templates/template.go @@ -2,9 +2,9 @@ package templates import ( "bytes" + "github.com/cedar-policy/cedar-go" "github.com/cedar-policy/cedar-go/internal/parser" "github.com/cedar-policy/cedar-go/types" - internalast "github.com/cedar-policy/cedar-go/x/exp/ast" ) // Template represents a Cedar policy template that can be linked with slot values @@ -30,11 +30,11 @@ func (p *Template) SetFilename(fileName string) { // LinkedPolicy represents a template that has been linked with specific slot values. // It's a wrapper around the internal parser.LinkedPolicy type. -type LinkedPolicy parser.LinkedPolicy +//type LinkedPolicy parser.LinkedPolicy -type LinkedPolicy2 struct { - templateID PolicyID - linkID PolicyID +type LinkedPolicy struct { + templateID cedar.PolicyID + linkID cedar.PolicyID slotEnv map[types.SlotID]types.EntityUID } @@ -46,58 +46,22 @@ type LinkedPolicy2 struct { // - slotEnv: A map of slot IDs to entity UIDs that will be substituted into the template // // Returns a LinkedPolicy that can be rendered into a concrete Policy. -func (p *PolicySet) LinkTemplate(templateID PolicyID, linkID PolicyID, slotEnv map[types.SlotID]types.EntityUID) LinkedPolicy2 { - link := LinkedPolicy2{templateID, linkID, slotEnv} +func (p *PolicySet) LinkTemplate(templateID cedar.PolicyID, linkID cedar.PolicyID, slotEnv map[types.SlotID]types.EntityUID) LinkedPolicy { + link := LinkedPolicy{templateID, linkID, slotEnv} p.links[linkID] = &link return link } -// Render converts a LinkedPolicy into a concrete Policy by substituting all slot values. -// Returns the rendered Policy and any error that occurred during rendering. -// If rendering fails (e.g., due to missing slot values), an error is returned. -func (p LinkedPolicy) Render() (*Policy, error) { - pl := parser.LinkedPolicy(p) - - policy, err := pl.Render() - if err != nil { - return nil, err - } - - internalPolicy := internalast.Policy(policy) - - return newPolicy(&internalPolicy), nil -} - -// MarshalJSON serializes the LinkedPolicy into its JSON representation. -// Returns the JSON representation as a byte slice and any error that occurred during marshaling. -func (p LinkedPolicy) MarshalJSON() ([]byte, error) { - pl := parser.LinkedPolicy(p) - - return pl.MarshalJSON() -} - -// AddLinkedPolicy renders a LinkedPolicy and adds the resulting concrete Policy to the PolicySet. -// The policy is added with the LinkID from the LinkedPolicy as its PolicyID. -// If rendering fails, no policy is added to the set. -//func (p *PolicySet) AddLinkedPolicy(lp LinkedPolicy) { -// policy, err := lp.Render() -// if err != nil { -// return -// } -// -// p.Add(PolicyID(lp.LinkID), policy) -//} - // GetTemplate returns the Template with the given ID. // If a template with the given ID does not exist, nil is returned. -func (p PolicySet) GetTemplate(templateID PolicyID) *Template { +func (p PolicySet) GetTemplate(templateID cedar.PolicyID) *Template { return p.templates[templateID] } // AddTemplate inserts or updates a template with the given ID. // Returns true if a template with the given ID did not already exist in the set. -func (p *PolicySet) AddTemplate(templateID PolicyID, template *Template) bool { +func (p *PolicySet) AddTemplate(templateID cedar.PolicyID, template *Template) bool { _, exists := p.templates[templateID] p.templates[templateID] = template return !exists @@ -105,7 +69,7 @@ func (p *PolicySet) AddTemplate(templateID PolicyID, template *Template) bool { // RemoveTemplate removes a template from the PolicySet. // Returns true if a template with the given ID already existed in the set. -func (p *PolicySet) RemoveTemplate(templateID PolicyID) bool { +func (p *PolicySet) RemoveTemplate(templateID cedar.PolicyID) bool { _, exists := p.templates[templateID] delete(p.templates, templateID) return exists From 195cb7018cc4e8933b4653160e6002a63e1cdef1 Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Wed, 14 May 2025 13:54:59 -0300 Subject: [PATCH 11/24] feat: return error on link template when conditions are invalid Signed-off-by: Caio Ferreira --- x/exp/templates/authorize_test.go | 181 +++++++++++++++++++++++++++++- x/exp/templates/template.go | 27 ++++- 2 files changed, 205 insertions(+), 3 deletions(-) diff --git a/x/exp/templates/authorize_test.go b/x/exp/templates/authorize_test.go index da84377e..35df9dd5 100644 --- a/x/exp/templates/authorize_test.go +++ b/x/exp/templates/authorize_test.go @@ -24,6 +24,7 @@ func TestIsAuthorizedFromLinkedPolicies(t *testing.T) { Want cedar.Decision DiagErr int ParseErr bool + LinkErr bool }{ { Name: "simple-permit", @@ -38,7 +39,184 @@ func TestIsAuthorizedFromLinkedPolicies(t *testing.T) { Want: cedar.Allow, DiagErr: 0, }, + { + Name: "simple-forbid", + Policy: `forbid(principal == ?principal,action,resource);`, + TemplateID: "template0", + LinkEnv: map[types.SlotID]types.EntityUID{"?principal": cuzco}, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: cedar.Deny, + DiagErr: 0, + }, + { + Name: "permit-resource-equals", + Policy: `permit(principal,action,resource == ?resource);`, + TemplateID: "template0", + LinkEnv: map[types.SlotID]types.EntityUID{"?resource": cedar.NewEntityUID("table", "whatever")}, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: cedar.Allow, + DiagErr: 0, + }, + { + Name: "permit-when-in-hierarchy", + Policy: `permit(principal in ?principal,action,resource);`, + TemplateID: "template0", + LinkEnv: map[types.SlotID]types.EntityUID{"?principal": cedar.NewEntityUID("team", "osiris")}, + Entities: cedar.EntityMap{ + cuzco: cedar.Entity{ + UID: cuzco, + Parents: cedar.NewEntityUIDSet(cedar.NewEntityUID("team", "osiris")), + }, + }, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: cedar.Allow, + DiagErr: 0, + }, + { + Name: "permit-when-condition", + Policy: `permit(principal == ?principal,action,resource) when { context.x == 42 };`, + TemplateID: "template0", + LinkEnv: map[types.SlotID]types.EntityUID{"?principal": cuzco}, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.NewRecord(cedar.RecordMap{"x": cedar.Long(42)}), + Want: cedar.Allow, + DiagErr: 0, + }, + { + Name: "permit-when-condition-fails", + Policy: `permit(principal == ?principal,action,resource) when { context.x == 42 };`, + TemplateID: "template0", + LinkEnv: map[types.SlotID]types.EntityUID{"?principal": cuzco}, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.NewRecord(cedar.RecordMap{"x": cedar.Long(43)}), + Want: cedar.Deny, + DiagErr: 0, + }, + { + Name: "permit-requires-entities", + Policy: `permit(principal == ?principal,action,resource) when { principal.x == 42 };`, + TemplateID: "template0", + LinkEnv: map[types.SlotID]types.EntityUID{"?principal": cuzco}, + Entities: cedar.EntityMap{ + cuzco: cedar.Entity{ + UID: cuzco, + Attributes: cedar.NewRecord(cedar.RecordMap{"x": cedar.Long(42)}), + }, + }, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: cedar.Allow, + DiagErr: 0, + }, + { + Name: "multiple-slots-without-action", + Policy: `permit(principal == ?principal,action,resource == ?resource);`, + TemplateID: "template0", + LinkEnv: map[types.SlotID]types.EntityUID{ + "?principal": cuzco, + "?resource": cedar.NewEntityUID("table", "whatever"), + }, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: cedar.Allow, + DiagErr: 0, + }, + { + Name: "incorrect-env-size", + Policy: `permit(principal == ?principal,action,resource == ?resource);`, + TemplateID: "template0", + LinkEnv: map[types.SlotID]types.EntityUID{ + "?principal": cuzco, + // Missing ?resource slot + }, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: cedar.Deny, + LinkErr: true, + }, + { + Name: "missing-template-slot", + Policy: `permit(principal == ?principal,action,resource == ?resource);`, + TemplateID: "template0", + LinkEnv: map[types.SlotID]types.EntityUID{ + "?resource": cedar.NewEntityUID("table", "whatever"), + }, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: cedar.Deny, + LinkErr: true, + }, + { + Name: "error-in-policy", + Policy: `permit(principal == ?principal,action,resource) when { resource in "foo" };`, + TemplateID: "template0", + LinkEnv: map[types.SlotID]types.EntityUID{"?principal": cuzco}, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: cedar.Deny, + DiagErr: 1, + }, + { + Name: "permit-unless", + Policy: `permit(principal == ?principal,action,resource) unless { context.x > 100 };`, + TemplateID: "template0", + LinkEnv: map[types.SlotID]types.EntityUID{"?principal": cuzco}, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.NewRecord(cedar.RecordMap{"x": cedar.Long(50)}), + Want: cedar.Allow, + DiagErr: 0, + }, + { + Name: "variable-used-in-wrong-place", + Policy: `permit(principal is coder,action,resource) when { principal == ?principal };`, + TemplateID: "template0", + LinkEnv: map[types.SlotID]types.EntityUID{"?principal": cuzco}, + Entities: cedar.EntityMap{}, + Principal: cuzco, + Action: dropTable, + Resource: cedar.NewEntityUID("table", "whatever"), + Context: cedar.Record{}, + Want: cedar.Deny, + DiagErr: 0, + ParseErr: true, + LinkErr: true, + }, } + for _, tt := range tests { tt := tt t.Run(tt.Name, func(t *testing.T) { @@ -46,7 +224,8 @@ func TestIsAuthorizedFromLinkedPolicies(t *testing.T) { ps, err := templates.NewPolicySetFromBytes("policy.cedar", []byte(tt.Policy)) testutil.Equals(t, err != nil, tt.ParseErr) - ps.LinkTemplate(tt.TemplateID, "link0", tt.LinkEnv) + err = ps.LinkTemplate(tt.TemplateID, "link0", tt.LinkEnv) + testutil.Equals(t, err != nil, tt.LinkErr) ok, diag := cedar.Authorize(ps, tt.Entities, cedar.Request{ Principal: tt.Principal, diff --git a/x/exp/templates/template.go b/x/exp/templates/template.go index 72c929ad..3648e688 100644 --- a/x/exp/templates/template.go +++ b/x/exp/templates/template.go @@ -2,9 +2,11 @@ package templates import ( "bytes" + "fmt" "github.com/cedar-policy/cedar-go" "github.com/cedar-policy/cedar-go/internal/parser" "github.com/cedar-policy/cedar-go/types" + "github.com/cedar-policy/cedar-go/x/exp/ast" ) // Template represents a Cedar policy template that can be linked with slot values @@ -28,6 +30,12 @@ func (p *Template) SetFilename(fileName string) { p.Position.Filename = fileName } +func (p *Template) Slots() []types.SlotID { + x := (*ast.Policy)(p) + + return x.Slots() +} + // LinkedPolicy represents a template that has been linked with specific slot values. // It's a wrapper around the internal parser.LinkedPolicy type. //type LinkedPolicy parser.LinkedPolicy @@ -46,11 +54,26 @@ type LinkedPolicy struct { // - slotEnv: A map of slot IDs to entity UIDs that will be substituted into the template // // Returns a LinkedPolicy that can be rendered into a concrete Policy. -func (p *PolicySet) LinkTemplate(templateID cedar.PolicyID, linkID cedar.PolicyID, slotEnv map[types.SlotID]types.EntityUID) LinkedPolicy { +func (p *PolicySet) LinkTemplate(templateID cedar.PolicyID, linkID cedar.PolicyID, slotEnv map[types.SlotID]types.EntityUID) error { + template := p.GetTemplate(templateID) + if template == nil { + return fmt.Errorf("template %s not found", templateID) + } + + if len(slotEnv) < len(template.Slots()) { + return fmt.Errorf("template %s requires %d variables, slot env has %d", templateID, len(template.Slots()), len(slotEnv)) + } + + for _, slotID := range template.Slots() { + if _, ok := slotEnv[slotID]; !ok { + return fmt.Errorf("template %s requires variable %s, missing from slot env", templateID, slotID) + } + } + link := LinkedPolicy{templateID, linkID, slotEnv} p.links[linkID] = &link - return link + return nil } // GetTemplate returns the Template with the given ID. From 608d6c56cd237f435eda54508f5437436ba85fd0 Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Sat, 7 Jun 2025 08:41:51 -0300 Subject: [PATCH 12/24] refact: remove variable slot type Signed-off-by: Caio Ferreira --- internal/eval/compile.go | 2 +- internal/json/json_marshal.go | 12 ++++++------ internal/json/json_test.go | 12 ++++++------ internal/json/json_unmarshal.go | 4 ++-- internal/parser/cedar_marshal.go | 4 ++-- internal/parser/cedar_unmarshal.go | 8 ++++---- internal/parser/cedar_unmarshal_test.go | 12 ++++++------ internal/parser/template.go | 4 ++-- types/entity.go | 7 ------- types/template.go | 2 ++ x/exp/ast/scope.go | 12 ++++++------ 11 files changed, 37 insertions(+), 42 deletions(-) diff --git a/internal/eval/compile.go b/internal/eval/compile.go index 15419c94..54448e2a 100644 --- a/internal/eval/compile.go +++ b/internal/eval/compile.go @@ -89,7 +89,7 @@ func entityReferenceToUID(ef types.EntityReference) types.EntityUID { switch e := ef.(type) { case types.EntityUID: return e - case types.VariableSlot: + case types.SlotID: panic("variable slot cannot be evaluated, you should instantiate a template-linked policy first") default: panic(fmt.Sprintf("unknown entity reference type %T", e)) diff --git a/internal/json/json_marshal.go b/internal/json/json_marshal.go index a2d54e02..7ff9dccc 100644 --- a/internal/json/json_marshal.go +++ b/internal/json/json_marshal.go @@ -19,8 +19,8 @@ func (s *scopeJSON) FromNode(src ast.IsScopeNode) { case types.EntityUID: e := types.ImplicitlyMarshaledEntityUID(ent) s.Entity = &e - case types.VariableSlot: - varName := ent.ID.String() + case types.SlotID: + varName := ent.String() s.Slot = &varName } @@ -31,8 +31,8 @@ func (s *scopeJSON) FromNode(src ast.IsScopeNode) { case types.EntityUID: e := types.ImplicitlyMarshaledEntityUID(ent) s.Entity = &e - case types.VariableSlot: - varName := ent.ID.String() + case types.SlotID: + varName := ent.String() s.Slot = &varName } @@ -58,8 +58,8 @@ func (s *scopeJSON) FromNode(src ast.IsScopeNode) { case types.EntityUID: uid := types.ImplicitlyMarshaledEntityUID(et) in.Entity = &uid - case types.VariableSlot: - varName := et.ID.String() + case types.SlotID: + varName := et.String() in.Slot = &varName } diff --git a/internal/json/json_test.go b/internal/json/json_test.go index c22c18b4..a44fba3c 100644 --- a/internal/json/json_test.go +++ b/internal/json/json_test.go @@ -478,37 +478,37 @@ func TestUnmarshalJSON(t *testing.T) { { "principal template variable", `{"effect":"permit","principal":{"op":"==", "slot": "?principal"},"action":{"op":"All"},"resource":{"op":"All"}}`, - ast.Permit().PrincipalEq(types.VariableSlot{ID: types.PrincipalSlot}).AddSlot(types.PrincipalSlot), + ast.Permit().PrincipalEq(types.PrincipalSlot).AddSlot(types.PrincipalSlot), testutil.OK, }, { "principal template variable with in operator", `{"effect":"permit","principal":{"op":"in", "slot": "?principal"},"action":{"op":"All"},"resource":{"op":"All"}}`, - ast.Permit().PrincipalIn(types.VariableSlot{ID: types.PrincipalSlot}).AddSlot(types.PrincipalSlot), + ast.Permit().PrincipalIn(types.PrincipalSlot).AddSlot(types.PrincipalSlot), testutil.OK, }, { "principal template variable with is in operator", `{"effect":"permit","principal":{"op":"is", "entity_type": "User", "in": {"slot": "?principal"} },"action":{"op":"All"},"resource":{"op":"All"}}`, - ast.Permit().PrincipalIsIn("User", types.VariableSlot{ID: types.PrincipalSlot}).AddSlot(types.PrincipalSlot), + ast.Permit().PrincipalIsIn("User", types.PrincipalSlot).AddSlot(types.PrincipalSlot), testutil.OK, }, { "resource template variable", `{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"==", "slot": "?resource"}}`, - ast.Permit().ResourceEq(types.VariableSlot{ID: types.ResourceSlot}).AddSlot(types.ResourceSlot), + ast.Permit().ResourceEq(types.ResourceSlot).AddSlot(types.ResourceSlot), testutil.OK, }, { "resource template variable with in operator", `{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"in", "slot": "?resource"}}`, - ast.Permit().ResourceIn(types.VariableSlot{ID: types.ResourceSlot}).AddSlot(types.ResourceSlot), + ast.Permit().ResourceIn(types.ResourceSlot).AddSlot(types.ResourceSlot), testutil.OK, }, { "resource template variable with is in operator", `{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"is", "entity_type": "Photo", "in": {"slot": "?resource"} }}`, - ast.Permit().ResourceIsIn("Photo", types.VariableSlot{ID: types.ResourceSlot}).AddSlot(types.ResourceSlot), + ast.Permit().ResourceIsIn("Photo", types.ResourceSlot).AddSlot(types.ResourceSlot), testutil.OK, }, { diff --git a/internal/json/json_unmarshal.go b/internal/json/json_unmarshal.go index 793d1217..c07f3368 100644 --- a/internal/json/json_unmarshal.go +++ b/internal/json/json_unmarshal.go @@ -49,7 +49,7 @@ func scopeEntityReference(s *scopeJSON) (types.EntityReference, error) { return nil, err } - ref = types.VariableSlot{ID: id} + ref = id case s.Entity != nil: ref = types.EntityUID(*s.Entity) default: @@ -77,7 +77,7 @@ func scopeInEntityReference(s *scopeInJSON) (types.EntityReference, error) { return nil, err } - ref = types.VariableSlot{ID: id} + ref = id case s.Entity != nil: ref = types.EntityUID(*s.Entity) default: diff --git a/internal/parser/cedar_marshal.go b/internal/parser/cedar_marshal.go index de83d04e..bb3790c3 100644 --- a/internal/parser/cedar_marshal.go +++ b/internal/parser/cedar_marshal.go @@ -73,8 +73,8 @@ func entityReferenceToNode(ef types.EntityReference) (ast.Node, error) { switch e := ef.(type) { case types.EntityUID: return ast.Value(e), nil - case types.VariableSlot: - return ast.NewNode(ast.NodeTypeVariable{Name: types.String(e.ID)}), nil + case types.SlotID: + return ast.NewNode(ast.NodeTypeVariable{Name: types.String(e.String())}), nil default: return ast.Node{}, errors.New("unknown entity reference type") } diff --git a/internal/parser/cedar_unmarshal.go b/internal/parser/cedar_unmarshal.go index 8b6c3fab..b5f76860 100644 --- a/internal/parser/cedar_unmarshal.go +++ b/internal/parser/cedar_unmarshal.go @@ -196,8 +196,8 @@ func (p *parser) effect(a *ast.Annotations) (*ast.Policy, error) { func addSlotToPolicy(entRef types.EntityReference, p *ast.Policy) { switch varSlot := entRef.(type) { - case types.VariableSlot: - p.AddSlot(varSlot.ID) + case types.SlotID: + p.AddSlot(varSlot) } } @@ -272,9 +272,9 @@ func (p *parser) entityReference() (types.EntityReference, error) { varName := "?" + t.Text switch varName { case string(types.PrincipalSlot): - return types.VariableSlot{ID: types.PrincipalSlot}, nil + return types.PrincipalSlot, nil case string(types.ResourceSlot): - return types.VariableSlot{ID: types.ResourceSlot}, nil + return types.ResourceSlot, nil } return nil, p.errorf("unknown variable name %v", varName) diff --git a/internal/parser/cedar_unmarshal_test.go b/internal/parser/cedar_unmarshal_test.go index 30261d72..0a94e08c 100644 --- a/internal/parser/cedar_unmarshal_test.go +++ b/internal/parser/cedar_unmarshal_test.go @@ -478,7 +478,7 @@ when { (if true then 2 else 3) * 4 == 8 };`, action, resource );`, - ast.Permit().PrincipalEq(types.VariableSlot{ID: types.PrincipalSlot}).AddSlot(types.PrincipalSlot), + ast.Permit().PrincipalEq(types.PrincipalSlot).AddSlot(types.PrincipalSlot), }, { "principal template variable with in operator", @@ -487,7 +487,7 @@ when { (if true then 2 else 3) * 4 == 8 };`, action, resource );`, - ast.Permit().PrincipalIn(types.VariableSlot{ID: types.PrincipalSlot}).AddSlot(types.PrincipalSlot), + ast.Permit().PrincipalIn(types.PrincipalSlot).AddSlot(types.PrincipalSlot), }, { "principal template variable with is in operator", @@ -496,7 +496,7 @@ when { (if true then 2 else 3) * 4 == 8 };`, action, resource );`, - ast.Permit().PrincipalIsIn("User", types.VariableSlot{ID: types.PrincipalSlot}).AddSlot(types.PrincipalSlot), + ast.Permit().PrincipalIsIn("User", types.PrincipalSlot).AddSlot(types.PrincipalSlot), }, { "resource template variable", @@ -505,7 +505,7 @@ when { (if true then 2 else 3) * 4 == 8 };`, action, resource == ?resource );`, - ast.Permit().ResourceEq(types.VariableSlot{ID: types.ResourceSlot}).AddSlot(types.ResourceSlot), + ast.Permit().ResourceEq(types.ResourceSlot).AddSlot(types.ResourceSlot), }, { "resource template variable with in operator", @@ -514,7 +514,7 @@ when { (if true then 2 else 3) * 4 == 8 };`, action, resource in ?resource );`, - ast.Permit().ResourceIn(types.VariableSlot{ID: types.ResourceSlot}).AddSlot(types.ResourceSlot), + ast.Permit().ResourceIn(types.ResourceSlot).AddSlot(types.ResourceSlot), }, { "resource template variable with is in operator", @@ -523,7 +523,7 @@ when { (if true then 2 else 3) * 4 == 8 };`, action, resource is Photo in ?resource );`, - ast.Permit().ResourceIsIn("Photo", types.VariableSlot{ID: types.ResourceSlot}).AddSlot(types.ResourceSlot), + ast.Permit().ResourceIsIn("Photo", types.ResourceSlot).AddSlot(types.ResourceSlot), }, } diff --git a/internal/parser/template.go b/internal/parser/template.go index dc6d0dd9..602269d1 100644 --- a/internal/parser/template.go +++ b/internal/parser/template.go @@ -103,8 +103,8 @@ func resolveSlot(ef types.EntityReference, slotEnv map[types.SlotID]types.Entity switch e := ef.(type) { case types.EntityUID: return e - case types.VariableSlot: - return slotEnv[e.ID] + case types.SlotID: + return slotEnv[e] default: panic(fmt.Sprintf("unknown entity reference type %T", e)) } diff --git a/types/entity.go b/types/entity.go index 09c8381c..8b3d7506 100644 --- a/types/entity.go +++ b/types/entity.go @@ -48,10 +48,3 @@ func (e Entity) MarshalJSON() ([]byte, error) { type EntityReference interface { isEntityReference() } - -type VariableSlot struct { - ID SlotID `json:"slot"` - Name String `json:"name"` -} - -func (v VariableSlot) isEntityReference() {} diff --git a/types/template.go b/types/template.go index a17e5a87..e606aa28 100644 --- a/types/template.go +++ b/types/template.go @@ -10,3 +10,5 @@ const ( func (s SlotID) String() string { return string(s) } + +func (s SlotID) isEntityReference() {} diff --git a/x/exp/ast/scope.go b/x/exp/ast/scope.go index 4f99b50b..d0b6a388 100644 --- a/x/exp/ast/scope.go +++ b/x/exp/ast/scope.go @@ -141,8 +141,8 @@ type ScopeTypeEq struct { func (t ScopeTypeEq) Slot() (slotID types.SlotID, found bool) { switch et := t.Entity.(type) { - case types.VariableSlot: - slotID = et.ID + case types.SlotID: + slotID = et found = true } @@ -159,8 +159,8 @@ type ScopeTypeIn struct { func (t ScopeTypeIn) Slot() (slotID types.SlotID, found bool) { switch et := t.Entity.(type) { - case types.VariableSlot: - slotID = et.ID + case types.SlotID: + slotID = et found = true } @@ -194,8 +194,8 @@ type ScopeTypeIsIn struct { func (t ScopeTypeIsIn) Slot() (slotID types.SlotID, found bool) { switch et := t.Entity.(type) { - case types.VariableSlot: - slotID = et.ID + case types.SlotID: + slotID = et found = true } From 48003461ed37f0c08c3437529eed2483b974af1e Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Sat, 7 Jun 2025 09:03:39 -0300 Subject: [PATCH 13/24] refact: make add slot private and use builder methods to manage slots Signed-off-by: Caio Ferreira --- internal/json/json_test.go | 12 +-- internal/json/json_unmarshal.go | 106 +++++++++++++++++------- internal/parser/cedar_unmarshal.go | 13 --- internal/parser/cedar_unmarshal_test.go | 12 +-- x/exp/ast/policy.go | 8 +- x/exp/ast/scope.go | 14 ++-- 6 files changed, 104 insertions(+), 61 deletions(-) diff --git a/internal/json/json_test.go b/internal/json/json_test.go index a44fba3c..89c4a7c5 100644 --- a/internal/json/json_test.go +++ b/internal/json/json_test.go @@ -478,37 +478,37 @@ func TestUnmarshalJSON(t *testing.T) { { "principal template variable", `{"effect":"permit","principal":{"op":"==", "slot": "?principal"},"action":{"op":"All"},"resource":{"op":"All"}}`, - ast.Permit().PrincipalEq(types.PrincipalSlot).AddSlot(types.PrincipalSlot), + ast.Permit().PrincipalEq(types.PrincipalSlot), testutil.OK, }, { "principal template variable with in operator", `{"effect":"permit","principal":{"op":"in", "slot": "?principal"},"action":{"op":"All"},"resource":{"op":"All"}}`, - ast.Permit().PrincipalIn(types.PrincipalSlot).AddSlot(types.PrincipalSlot), + ast.Permit().PrincipalIn(types.PrincipalSlot), testutil.OK, }, { "principal template variable with is in operator", `{"effect":"permit","principal":{"op":"is", "entity_type": "User", "in": {"slot": "?principal"} },"action":{"op":"All"},"resource":{"op":"All"}}`, - ast.Permit().PrincipalIsIn("User", types.PrincipalSlot).AddSlot(types.PrincipalSlot), + ast.Permit().PrincipalIsIn("User", types.PrincipalSlot), testutil.OK, }, { "resource template variable", `{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"==", "slot": "?resource"}}`, - ast.Permit().ResourceEq(types.ResourceSlot).AddSlot(types.ResourceSlot), + ast.Permit().ResourceEq(types.ResourceSlot), testutil.OK, }, { "resource template variable with in operator", `{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"in", "slot": "?resource"}}`, - ast.Permit().ResourceIn(types.ResourceSlot).AddSlot(types.ResourceSlot), + ast.Permit().ResourceIn(types.ResourceSlot), testutil.OK, }, { "resource template variable with is in operator", `{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"is", "entity_type": "Photo", "in": {"slot": "?resource"} }}`, - ast.Permit().ResourceIsIn("Photo", types.ResourceSlot).AddSlot(types.ResourceSlot), + ast.Permit().ResourceIsIn("Photo", types.ResourceSlot), testutil.OK, }, { diff --git a/internal/json/json_unmarshal.go b/internal/json/json_unmarshal.go index c07f3368..aa184bc8 100644 --- a/internal/json/json_unmarshal.go +++ b/internal/json/json_unmarshal.go @@ -87,38 +87,88 @@ func scopeInEntityReference(s *scopeInJSON) (types.EntityReference, error) { return ref, nil } -func (s *scopeJSON) ToPrincipalResourceNode() (isPrincipalResourceScopeNode, error) { +func (s *scopeJSON) ToPrincipalNode(policy *Policy) error { switch s.Op { case "All": - return ast.Scope{}.All(), nil + return nil case "==": ref, err := scopeEntityReference(s) if err != nil { - return nil, err + return err } - return ast.Scope{}.Eq(ref), nil + policy.unwrap().PrincipalEq(ref) + + return nil case "in": ref, err := scopeEntityReference(s) if err != nil { - return nil, err + return err } - return ast.Scope{}.In(ref), nil + policy.unwrap().PrincipalIn(ref) + + return nil case "is": if s.In == nil { - return ast.Scope{}.Is(types.EntityType(s.EntityType)), nil + policy.unwrap().PrincipalIs(types.EntityType(s.EntityType)) + + return nil } ref, err := scopeInEntityReference(s.In) if err != nil { - return nil, err + return err } - return ast.Scope{}.IsIn(types.EntityType(s.EntityType), ref), nil + policy.unwrap().PrincipalIsIn(types.EntityType(s.EntityType), ref) + + return nil } - return nil, fmt.Errorf("unknown op: %v", s.Op) + return fmt.Errorf("unknown op: %v", s.Op) +} + +func (s *scopeJSON) ToResourceNode(policy *Policy) error { + switch s.Op { + case "All": + return nil + case "==": + ref, err := scopeEntityReference(s) + if err != nil { + return err + } + + policy.unwrap().ResourceEq(ref) + + return nil + case "in": + ref, err := scopeEntityReference(s) + if err != nil { + return err + } + + policy.unwrap().ResourceIn(ref) + + return nil + case "is": + if s.In == nil { + policy.unwrap().ResourceIs(types.EntityType(s.EntityType)) + + return nil + } + + ref, err := scopeInEntityReference(s.In) + if err != nil { + return err + } + + policy.unwrap().ResourceIsIn(types.EntityType(s.EntityType), ref) + + return nil + } + + return fmt.Errorf("unknown op: %v", s.Op) } func (s *scopeJSON) ToActionNode() (ast.IsActionScopeNode, error) { @@ -388,38 +438,38 @@ func (p *Policy) UnmarshalJSON(b []byte) error { p.unwrap().Annotate(types.Ident(k), types.String(v)) } - principal, err := j.Principal.ToPrincipalResourceNode() + err := j.Principal.ToPrincipalNode(p) if err != nil { return fmt.Errorf("error in principal: %w", err) } - p.Principal = principal - if slot, found := principal.Slot(); found { - if slot != types.PrincipalSlot { - return fmt.Errorf("variable used in principal slot is not %s", types.PrincipalSlot) - } - - p.unwrap().AddSlot(slot) - } + //p.Principal = principal + //if slot, found := principal.Slot(); found { + // if slot != types.PrincipalSlot { + // return fmt.Errorf("variable used in principal slot is not %s", types.PrincipalSlot) + // } + // + // p.unwrap().AddSlot(slot) + //} p.Action, err = j.Action.ToActionNode() if err != nil { return fmt.Errorf("error in action: %w", err) } - resource, err := j.Resource.ToPrincipalResourceNode() + err = j.Resource.ToResourceNode(p) if err != nil { return fmt.Errorf("error in resource: %w", err) } - p.Resource = resource - if slot, found := resource.Slot(); found { - if slot != types.ResourceSlot { - return fmt.Errorf("variable used in resource slot is not %s", types.ResourceSlot) - } - - p.unwrap().AddSlot(slot) - } + //p.Resource = resource + //if slot, found := resource.Slot(); found { + // if slot != types.ResourceSlot { + // return fmt.Errorf("variable used in resource slot is not %s", types.ResourceSlot) + // } + // + // p.unwrap().AddSlot(slot) + //} for _, c := range j.Conditions { n, err := c.Body.ToNode() diff --git a/internal/parser/cedar_unmarshal.go b/internal/parser/cedar_unmarshal.go index b5f76860..c8a03bbe 100644 --- a/internal/parser/cedar_unmarshal.go +++ b/internal/parser/cedar_unmarshal.go @@ -194,13 +194,6 @@ func (p *parser) effect(a *ast.Annotations) (*ast.Policy, error) { return nil, p.errorf("unexpected effect: %v", next.Text) } -func addSlotToPolicy(entRef types.EntityReference, p *ast.Policy) { - switch varSlot := entRef.(type) { - case types.SlotID: - p.AddSlot(varSlot) - } -} - func (p *parser) principal(policy *ast.Policy) error { if err := p.exact(consts.Principal); err != nil { return err @@ -213,7 +206,6 @@ func (p *parser) principal(policy *ast.Policy) error { return err } - addSlotToPolicy(entity, policy) policy.PrincipalEq(entity) return nil case "is": @@ -229,7 +221,6 @@ func (p *parser) principal(policy *ast.Policy) error { return err } - addSlotToPolicy(entity, policy) policy.PrincipalIsIn(path, entity) return nil } @@ -243,7 +234,6 @@ func (p *parser) principal(policy *ast.Policy) error { return err } - addSlotToPolicy(entity, policy) policy.PrincipalIn(entity) return nil } @@ -403,7 +393,6 @@ func (p *parser) resource(policy *ast.Policy) error { return err } - addSlotToPolicy(entity, policy) policy.ResourceEq(entity) return nil case "is": @@ -419,7 +408,6 @@ func (p *parser) resource(policy *ast.Policy) error { return err } - addSlotToPolicy(entity, policy) policy.ResourceIsIn(path, entity) return nil } @@ -433,7 +421,6 @@ func (p *parser) resource(policy *ast.Policy) error { return err } - addSlotToPolicy(entity, policy) policy.ResourceIn(entity) return nil } diff --git a/internal/parser/cedar_unmarshal_test.go b/internal/parser/cedar_unmarshal_test.go index 0a94e08c..c896bef7 100644 --- a/internal/parser/cedar_unmarshal_test.go +++ b/internal/parser/cedar_unmarshal_test.go @@ -478,7 +478,7 @@ when { (if true then 2 else 3) * 4 == 8 };`, action, resource );`, - ast.Permit().PrincipalEq(types.PrincipalSlot).AddSlot(types.PrincipalSlot), + ast.Permit().PrincipalEq(types.PrincipalSlot), }, { "principal template variable with in operator", @@ -487,7 +487,7 @@ when { (if true then 2 else 3) * 4 == 8 };`, action, resource );`, - ast.Permit().PrincipalIn(types.PrincipalSlot).AddSlot(types.PrincipalSlot), + ast.Permit().PrincipalIn(types.PrincipalSlot), }, { "principal template variable with is in operator", @@ -496,7 +496,7 @@ when { (if true then 2 else 3) * 4 == 8 };`, action, resource );`, - ast.Permit().PrincipalIsIn("User", types.PrincipalSlot).AddSlot(types.PrincipalSlot), + ast.Permit().PrincipalIsIn("User", types.PrincipalSlot), }, { "resource template variable", @@ -505,7 +505,7 @@ when { (if true then 2 else 3) * 4 == 8 };`, action, resource == ?resource );`, - ast.Permit().ResourceEq(types.ResourceSlot).AddSlot(types.ResourceSlot), + ast.Permit().ResourceEq(types.ResourceSlot), }, { "resource template variable with in operator", @@ -514,7 +514,7 @@ when { (if true then 2 else 3) * 4 == 8 };`, action, resource in ?resource );`, - ast.Permit().ResourceIn(types.ResourceSlot).AddSlot(types.ResourceSlot), + ast.Permit().ResourceIn(types.ResourceSlot), }, { "resource template variable with is in operator", @@ -523,7 +523,7 @@ when { (if true then 2 else 3) * 4 == 8 };`, action, resource is Photo in ?resource );`, - ast.Permit().ResourceIsIn("Photo", types.ResourceSlot).AddSlot(types.ResourceSlot), + ast.Permit().ResourceIsIn("Photo", types.ResourceSlot), }, } diff --git a/x/exp/ast/policy.go b/x/exp/ast/policy.go index 0427de3a..3074d9e5 100644 --- a/x/exp/ast/policy.go +++ b/x/exp/ast/policy.go @@ -80,8 +80,12 @@ func (p *Policy) Unless(node Node) *Policy { return p } -func (p *Policy) AddSlot(slotID types.SlotID) *Policy { - p.tplCtx.slots = append(p.tplCtx.slots, slotID) +func (p *Policy) addSlot(entRef types.EntityReference) *Policy { + switch v := entRef.(type) { + case types.SlotID: + p.tplCtx.slots = append(p.tplCtx.slots, v) + } + return p } diff --git a/x/exp/ast/scope.go b/x/exp/ast/scope.go index d0b6a388..4bd69e0d 100644 --- a/x/exp/ast/scope.go +++ b/x/exp/ast/scope.go @@ -32,22 +32,24 @@ func (s Scope) IsIn(entityType types.EntityType, entity types.EntityReference) S func (p *Policy) PrincipalEq(entity types.EntityReference) *Policy { p.Principal = Scope{}.Eq(entity) - return p + return p.addSlot(entity) } func (p *Policy) PrincipalIn(entity types.EntityReference) *Policy { p.Principal = Scope{}.In(entity) - return p + return p.addSlot(entity) } func (p *Policy) PrincipalIs(entityType types.EntityType) *Policy { p.Principal = Scope{}.Is(entityType) + return p } func (p *Policy) PrincipalIsIn(entityType types.EntityType, entity types.EntityReference) *Policy { p.Principal = Scope{}.IsIn(entityType, entity) - return p + + return p.addSlot(entity) } func (p *Policy) ActionEq(entity types.EntityUID) *Policy { @@ -67,12 +69,12 @@ func (p *Policy) ActionInSet(entities ...types.EntityUID) *Policy { func (p *Policy) ResourceEq(entity types.EntityReference) *Policy { p.Resource = Scope{}.Eq(entity) - return p + return p.addSlot(entity) } func (p *Policy) ResourceIn(entity types.EntityReference) *Policy { p.Resource = Scope{}.In(entity) - return p + return p.addSlot(entity) } func (p *Policy) ResourceIs(entityType types.EntityType) *Policy { @@ -82,7 +84,7 @@ func (p *Policy) ResourceIs(entityType types.EntityType) *Policy { func (p *Policy) ResourceIsIn(entityType types.EntityType, entity types.EntityReference) *Policy { p.Resource = Scope{}.IsIn(entityType, entity) - return p + return p.addSlot(entity) } type IsScopeNode interface { From 536bddcaaa433b89895cbad11169676006e59b5c Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Sat, 7 Jun 2025 09:15:45 -0300 Subject: [PATCH 14/24] refact: remove Slot method from interface and validate when parsing it Signed-off-by: Caio Ferreira --- internal/json/json_unmarshal.go | 60 ++++++++++++++++++++------------- x/exp/ast/scope.go | 38 --------------------- 2 files changed, 37 insertions(+), 61 deletions(-) diff --git a/internal/json/json_unmarshal.go b/internal/json/json_unmarshal.go index aa184bc8..7d3cea5b 100644 --- a/internal/json/json_unmarshal.go +++ b/internal/json/json_unmarshal.go @@ -15,7 +15,6 @@ import ( type isPrincipalResourceScopeNode interface { ast.IsPrincipalScopeNode ast.IsResourceScopeNode - Slot() (types.SlotID, bool) } func slotID(id *string) (types.SlotID, error) { @@ -87,7 +86,16 @@ func scopeInEntityReference(s *scopeInJSON) (types.EntityReference, error) { return ref, nil } -func (s *scopeJSON) ToPrincipalNode(policy *Policy) error { +func isSlotValid(entRef types.EntityReference, slot types.SlotID) bool { + switch entRef.(type) { + case types.SlotID: + return slot == types.PrincipalSlot + default: + return true + } +} + +func (s *scopeJSON) ToPrincipalNode(policy *Policy, allowedSlot types.SlotID) error { switch s.Op { case "All": return nil @@ -97,6 +105,10 @@ func (s *scopeJSON) ToPrincipalNode(policy *Policy) error { return err } + if !isSlotValid(ref, allowedSlot) { + return fmt.Errorf("variable used in principal slot is not %s", allowedSlot) + } + policy.unwrap().PrincipalEq(ref) return nil @@ -106,6 +118,10 @@ func (s *scopeJSON) ToPrincipalNode(policy *Policy) error { return err } + if !isSlotValid(ref, allowedSlot) { + return fmt.Errorf("variable used in principal slot is not %s", allowedSlot) + } + policy.unwrap().PrincipalIn(ref) return nil @@ -121,6 +137,10 @@ func (s *scopeJSON) ToPrincipalNode(policy *Policy) error { return err } + if !isSlotValid(ref, allowedSlot) { + return fmt.Errorf("variable used in principal slot is not %s", allowedSlot) + } + policy.unwrap().PrincipalIsIn(types.EntityType(s.EntityType), ref) return nil @@ -129,7 +149,7 @@ func (s *scopeJSON) ToPrincipalNode(policy *Policy) error { return fmt.Errorf("unknown op: %v", s.Op) } -func (s *scopeJSON) ToResourceNode(policy *Policy) error { +func (s *scopeJSON) ToResourceNode(policy *Policy, allowedSlot types.SlotID) error { switch s.Op { case "All": return nil @@ -139,6 +159,10 @@ func (s *scopeJSON) ToResourceNode(policy *Policy) error { return err } + if !isSlotValid(ref, allowedSlot) { + return fmt.Errorf("variable used in principal slot is not %s", allowedSlot) + } + policy.unwrap().ResourceEq(ref) return nil @@ -148,6 +172,10 @@ func (s *scopeJSON) ToResourceNode(policy *Policy) error { return err } + if !isSlotValid(ref, allowedSlot) { + return fmt.Errorf("variable used in principal slot is not %s", allowedSlot) + } + policy.unwrap().ResourceIn(ref) return nil @@ -163,6 +191,10 @@ func (s *scopeJSON) ToResourceNode(policy *Policy) error { return err } + if !isSlotValid(ref, allowedSlot) { + return fmt.Errorf("variable used in principal slot is not %s", allowedSlot) + } + policy.unwrap().ResourceIsIn(types.EntityType(s.EntityType), ref) return nil @@ -438,39 +470,21 @@ func (p *Policy) UnmarshalJSON(b []byte) error { p.unwrap().Annotate(types.Ident(k), types.String(v)) } - err := j.Principal.ToPrincipalNode(p) + err := j.Principal.ToPrincipalNode(p, types.PrincipalSlot) if err != nil { return fmt.Errorf("error in principal: %w", err) } - //p.Principal = principal - //if slot, found := principal.Slot(); found { - // if slot != types.PrincipalSlot { - // return fmt.Errorf("variable used in principal slot is not %s", types.PrincipalSlot) - // } - // - // p.unwrap().AddSlot(slot) - //} - p.Action, err = j.Action.ToActionNode() if err != nil { return fmt.Errorf("error in action: %w", err) } - err = j.Resource.ToResourceNode(p) + err = j.Resource.ToResourceNode(p, types.ResourceSlot) if err != nil { return fmt.Errorf("error in resource: %w", err) } - //p.Resource = resource - //if slot, found := resource.Slot(); found { - // if slot != types.ResourceSlot { - // return fmt.Errorf("variable used in resource slot is not %s", types.ResourceSlot) - // } - // - // p.unwrap().AddSlot(slot) - //} - for _, c := range j.Conditions { n, err := c.Body.ToNode() if err != nil { diff --git a/x/exp/ast/scope.go b/x/exp/ast/scope.go index 4bd69e0d..55d95bb6 100644 --- a/x/exp/ast/scope.go +++ b/x/exp/ast/scope.go @@ -129,10 +129,6 @@ type ScopeTypeAll struct { ResourceScopeNode } -func (t ScopeTypeAll) Slot() (slotID types.SlotID, found bool) { - return "", false -} - type ScopeTypeEq struct { ScopeNode PrincipalScopeNode @@ -141,16 +137,6 @@ type ScopeTypeEq struct { Entity types.EntityReference } -func (t ScopeTypeEq) Slot() (slotID types.SlotID, found bool) { - switch et := t.Entity.(type) { - case types.SlotID: - slotID = et - found = true - } - - return -} - type ScopeTypeIn struct { ScopeNode PrincipalScopeNode @@ -159,16 +145,6 @@ type ScopeTypeIn struct { Entity types.EntityReference } -func (t ScopeTypeIn) Slot() (slotID types.SlotID, found bool) { - switch et := t.Entity.(type) { - case types.SlotID: - slotID = et - found = true - } - - return -} - type ScopeTypeInSet struct { ScopeNode ActionScopeNode @@ -182,10 +158,6 @@ type ScopeTypeIs struct { Type types.EntityType } -func (t ScopeTypeIs) Slot() (slotID types.SlotID, found bool) { - return "", false -} - type ScopeTypeIsIn struct { ScopeNode PrincipalScopeNode @@ -193,13 +165,3 @@ type ScopeTypeIsIn struct { Type types.EntityType Entity types.EntityReference } - -func (t ScopeTypeIsIn) Slot() (slotID types.SlotID, found bool) { - switch et := t.Entity.(type) { - case types.SlotID: - slotID = et - found = true - } - - return -} From b178952eb659f1b47bc1bd2220e032a228f180d8 Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Sat, 7 Jun 2025 09:37:43 -0300 Subject: [PATCH 15/24] refact: replicate policy to templates package Signed-off-by: Caio Ferreira --- x/exp/templates/authorize.go | 64 ++++++++++++++++ x/exp/templates/authorize_test.go | 2 +- x/exp/templates/policy.go | 104 ++++++++++++++++++++++++++ x/exp/templates/policy_list.go | 7 +- x/exp/templates/policy_set.go | 120 ++++++++++++++++++++++++++---- x/exp/templates/template.go | 98 ------------------------ 6 files changed, 278 insertions(+), 117 deletions(-) create mode 100644 x/exp/templates/authorize.go create mode 100644 x/exp/templates/policy.go diff --git a/x/exp/templates/authorize.go b/x/exp/templates/authorize.go new file mode 100644 index 00000000..2a74099a --- /dev/null +++ b/x/exp/templates/authorize.go @@ -0,0 +1,64 @@ +package templates + +import ( + "github.com/cedar-policy/cedar-go" + "iter" + + "github.com/cedar-policy/cedar-go/internal/eval" + "github.com/cedar-policy/cedar-go/types" +) + +// PolicyIterator is an interface which abstracts an iterable set of policies. +type PolicyIterator interface { + // All returns an iterator over all the policies in the set + All() iter.Seq2[cedar.PolicyID, *Policy] +} + +// Authorize uses the combination of the PolicySet and Entities to determine +// if the given Request to determine Decision and Diagnostic. +func Authorize(policies PolicyIterator, entities types.EntityGetter, req cedar.Request) (cedar.Decision, cedar.Diagnostic) { + if entities == nil { + var zero types.EntityMap + entities = zero + } + env := eval.Env{ + Entities: entities, + Principal: req.Principal, + Action: req.Action, + Resource: req.Resource, + Context: req.Context, + } + var diag cedar.Diagnostic + var forbids []cedar.DiagnosticReason + var permits []cedar.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 policies.All() { + result, err := po.eval.Eval(env) + if err != nil { + diag.Errors = append(diag.Errors, cedar.DiagnosticError{PolicyID: id, Position: po.Position(), Message: err.Error()}) + continue + } + if !result { + continue + } + if po.Effect() == cedar.Forbid { + forbids = append(forbids, cedar.DiagnosticReason{PolicyID: id, Position: po.Position()}) + } else { + permits = append(permits, cedar.DiagnosticReason{PolicyID: id, Position: po.Position()}) + } + } + if len(forbids) > 0 { + diag.Reasons = forbids + return cedar.Deny, diag + } + if len(permits) > 0 { + diag.Reasons = permits + return cedar.Allow, diag + } + + return cedar.Deny, diag +} diff --git a/x/exp/templates/authorize_test.go b/x/exp/templates/authorize_test.go index 35df9dd5..66122448 100644 --- a/x/exp/templates/authorize_test.go +++ b/x/exp/templates/authorize_test.go @@ -227,7 +227,7 @@ func TestIsAuthorizedFromLinkedPolicies(t *testing.T) { err = ps.LinkTemplate(tt.TemplateID, "link0", tt.LinkEnv) testutil.Equals(t, err != nil, tt.LinkErr) - ok, diag := cedar.Authorize(ps, tt.Entities, cedar.Request{ + ok, diag := templates.Authorize(ps, tt.Entities, cedar.Request{ Principal: tt.Principal, Action: tt.Action, Resource: tt.Resource, diff --git a/x/exp/templates/policy.go b/x/exp/templates/policy.go new file mode 100644 index 00000000..483edf41 --- /dev/null +++ b/x/exp/templates/policy.go @@ -0,0 +1,104 @@ +package templates + +import ( + "bytes" + "github.com/cedar-policy/cedar-go" + + "github.com/cedar-policy/cedar-go/ast" + "github.com/cedar-policy/cedar-go/internal/eval" + "github.com/cedar-policy/cedar-go/internal/json" + "github.com/cedar-policy/cedar-go/internal/parser" + internalast "github.com/cedar-policy/cedar-go/x/exp/ast" +) + +// A Policy is the parsed form of a single Cedar language policy statement. +type Policy struct { + eval eval.BoolEvaler // determines if a policy matches a request. + ast *internalast.Policy +} + +func newPolicy(astIn *internalast.Policy) *Policy { + return &Policy{eval: eval.Compile(astIn), ast: astIn} +} + +// 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.ast) + 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 = *newPolicy((*internalast.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.ast) + + 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 = *newPolicy((*internalast.Policy)(&cedarPolicy)) + return nil +} + +// NewPolicyFromAST lets you create a new policy statement from a programmatically created AST. +// Do not modify the *ast.Policy after passing it into NewPolicyFromAST. +func NewPolicyFromAST(astIn *ast.Policy) *Policy { + p := newPolicy((*internalast.Policy)(astIn)) + return p +} + +// Annotations retrieves the annotations associated with this policy. +func (p *Policy) Annotations() cedar.Annotations { + res := make(cedar.Annotations, len(p.ast.Annotations)) + for _, e := range p.ast.Annotations { + res[e.Key] = e.Value + } + return res +} + +// Effect retrieves the effect of this policy. +func (p *Policy) Effect() cedar.Effect { + return cedar.Effect(p.ast.Effect) +} + +// Position retrieves the position of this policy. +func (p *Policy) Position() cedar.Position { + return cedar.Position(p.ast.Position) +} + +// SetFilename sets the filename of this policy. +func (p *Policy) SetFilename(fileName string) { + p.ast.Position.Filename = fileName +} + +// AST retrieves the AST of this policy. Do not modify the AST, as the +// compiled policy will no longer be in sync with the AST. +func (p *Policy) AST() *ast.Policy { + return (*ast.Policy)(p.ast) +} diff --git a/x/exp/templates/policy_list.go b/x/exp/templates/policy_list.go index 3dee9b90..a11bb910 100644 --- a/x/exp/templates/policy_list.go +++ b/x/exp/templates/policy_list.go @@ -3,7 +3,6 @@ package templates import ( "bytes" "fmt" - "github.com/cedar-policy/cedar-go" "github.com/cedar-policy/cedar-go/ast" "github.com/cedar-policy/cedar-go/internal/parser" ) @@ -11,7 +10,7 @@ import ( // PolicyList represents a list of un-named Policy's. Cedar documents, unlike the PolicySet form, don't have a means of // naming individual policies. type PolicyList struct { - StaticPolicies []*cedar.Policy + StaticPolicies []*Policy Templates []*Template } @@ -41,9 +40,9 @@ func (p *PolicyList) UnmarshalCedar(b []byte) error { return fmt.Errorf("parser error: %w", err) } - staticPolicies := make([]*cedar.Policy, 0, len(res.StaticPolicies)) + staticPolicies := make([]*Policy, 0, len(res.StaticPolicies)) for _, p := range res.StaticPolicies { - newPolicy := cedar.NewPolicyFromAST((*ast.Policy)(p)) + newPolicy := NewPolicyFromAST((*ast.Policy)(p)) staticPolicies = append(staticPolicies, newPolicy) } diff --git a/x/exp/templates/policy_set.go b/x/exp/templates/policy_set.go index 2fa0ed67..d3449a38 100644 --- a/x/exp/templates/policy_set.go +++ b/x/exp/templates/policy_set.go @@ -6,8 +6,9 @@ import ( "encoding/json" "fmt" "github.com/cedar-policy/cedar-go" - "github.com/cedar-policy/cedar-go/ast" "github.com/cedar-policy/cedar-go/internal/parser" + "github.com/cedar-policy/cedar-go/types" + internalast "github.com/cedar-policy/cedar-go/x/exp/ast" "iter" "maps" "slices" @@ -15,10 +16,12 @@ import ( internaljson "github.com/cedar-policy/cedar-go/internal/json" ) +type PolicyMap map[cedar.PolicyID]*Policy + // PolicySet is a set of named policies against which a request can be authorized. type PolicySet struct { // policies are stored internally so we can handle performance, concurrency bookkeeping however we want - policies cedar.PolicyMap + policies PolicyMap templates map[cedar.PolicyID]*Template links map[cedar.PolicyID]*LinkedPolicy @@ -27,7 +30,7 @@ type PolicySet struct { // NewPolicySet creates a new, empty PolicySet func NewPolicySet() *PolicySet { return &PolicySet{ - policies: cedar.PolicyMap{}, + policies: PolicyMap{}, templates: make(map[cedar.PolicyID]*Template), links: make(map[cedar.PolicyID]*LinkedPolicy), } @@ -43,7 +46,7 @@ func NewPolicySetFromBytes(fileName string, document []byte) (*PolicySet, error) if err != nil { return &PolicySet{}, err } - policyMap := make(cedar.PolicyMap, len(policySlice.StaticPolicies)) + policyMap := make(PolicyMap, len(policySlice.StaticPolicies)) for i, p := range policySlice.StaticPolicies { policyID := cedar.PolicyID(fmt.Sprintf("policy%d", i)) policyMap[policyID] = p @@ -60,13 +63,13 @@ func NewPolicySetFromBytes(fileName string, document []byte) (*PolicySet, error) // Get returns the Policy with the given ID. If a policy with the given ID // does not exist, nil is returned. -func (p *PolicySet) Get(policyID cedar.PolicyID) *cedar.Policy { +func (p *PolicySet) Get(policyID cedar.PolicyID) *Policy { return p.policies[policyID] } // Add inserts or updates a policy with the given ID. Returns true if a policy // with the given ID did not already exist in the set. -func (p *PolicySet) Add(policyID cedar.PolicyID, policy *cedar.Policy) bool { +func (p *PolicySet) Add(policyID cedar.PolicyID, policy *Policy) bool { _, exists := p.policies[policyID] p.policies[policyID] = policy return !exists @@ -83,7 +86,7 @@ func (p *PolicySet) Remove(policyID cedar.PolicyID) bool { // Map returns a new PolicyMap instance of the policies in the PolicySet. // // Deprecated: use the iterator returned by All() like so: maps.Collect(ps.All()) -func (p *PolicySet) Map() cedar.PolicyMap { +func (p *PolicySet) Map() PolicyMap { return maps.Clone(p.policies) } @@ -132,17 +135,17 @@ func (p *PolicySet) UnmarshalJSON(b []byte) error { return err } *p = PolicySet{ - policies: make(cedar.PolicyMap, len(jsonPolicySet.StaticPolicies)), + policies: make(PolicyMap, len(jsonPolicySet.StaticPolicies)), } for k, v := range jsonPolicySet.StaticPolicies { - p.policies[cedar.PolicyID(k)] = cedar.NewPolicyFromAST((*ast.Policy)(v)) + p.policies[cedar.PolicyID(k)] = newPolicy((*internalast.Policy)(v)) // NewPolicyFromAST((*ast.Policy)(v)) } return nil } // All returns an iterator over the (PolicyID, *Policy) tuples in the PolicySet -func (p *PolicySet) All() iter.Seq2[cedar.PolicyID, *cedar.Policy] { - return func(yield func(cedar.PolicyID, *cedar.Policy) bool) { +func (p *PolicySet) All() iter.Seq2[cedar.PolicyID, *Policy] { + return func(yield func(cedar.PolicyID, *Policy) bool) { for k, v := range p.policies { if !yield(k, v) { break @@ -163,7 +166,7 @@ func (p *PolicySet) All() iter.Seq2[cedar.PolicyID, *cedar.Policy] { } } -func (p *PolicySet) render(link LinkedPolicy) (*cedar.Policy, error) { +func (p *PolicySet) render(link LinkedPolicy) (*Policy, error) { template := p.GetTemplate(link.templateID) if template == nil { return nil, fmt.Errorf("no such template %q", link.templateID) @@ -176,7 +179,96 @@ func (p *PolicySet) render(link LinkedPolicy) (*cedar.Policy, error) { return nil, err } - astPolicy := ast.Policy(policy) + astPolicy := internalast.Policy(policy) + + return newPolicy(&astPolicy), nil +} + +// Template represents a Cedar policy template that can be linked with slot values +// to create concrete policies. It's a wrapper around the internal parser.Policy type. +type Template parser.Policy + +// MarshalCedar serializes the Template into its Cedar language representation. +// Returns the serialized template as a byte slice. +func (p *Template) MarshalCedar() []byte { + cedarPolicy := (*parser.Policy)(p) + + var buf bytes.Buffer + cedarPolicy.MarshalCedar(&buf) + + return buf.Bytes() +} - return cedar.NewPolicyFromAST(&astPolicy), nil +// SetFilename sets the filename of this template. +// This is useful for error reporting and debugging purposes. +func (p *Template) SetFilename(fileName string) { + p.Position.Filename = fileName +} + +func (p *Template) Slots() []types.SlotID { + x := (*internalast.Policy)(p) + + return x.Slots() +} + +// LinkedPolicy represents a template that has been linked with specific slot values. +// It's a wrapper around the internal parser.LinkedPolicy type. +//type LinkedPolicy parser.LinkedPolicy + +type LinkedPolicy struct { + templateID cedar.PolicyID + linkID cedar.PolicyID + slotEnv map[types.SlotID]types.EntityUID +} + +// LinkTemplate creates a LinkedPolicy by binding slot values to a template. +// Parameters: +// - template: The policy template to link +// - templateID: The identifier for the template +// - linkID: The identifier for the resulting linked policy +// - slotEnv: A map of slot IDs to entity UIDs that will be substituted into the template +// +// Returns a LinkedPolicy that can be rendered into a concrete Policy. +func (p *PolicySet) LinkTemplate(templateID cedar.PolicyID, linkID cedar.PolicyID, slotEnv map[types.SlotID]types.EntityUID) error { + template := p.GetTemplate(templateID) + if template == nil { + return fmt.Errorf("template %s not found", templateID) + } + + if len(slotEnv) < len(template.Slots()) { + return fmt.Errorf("template %s requires %d variables, slot env has %d", templateID, len(template.Slots()), len(slotEnv)) + } + + for _, slotID := range template.Slots() { + if _, ok := slotEnv[slotID]; !ok { + return fmt.Errorf("template %s requires variable %s, missing from slot env", templateID, slotID) + } + } + + link := LinkedPolicy{templateID, linkID, slotEnv} + p.links[linkID] = &link + + return nil +} + +// GetTemplate returns the Template with the given ID. +// If a template with the given ID does not exist, nil is returned. +func (p PolicySet) GetTemplate(templateID cedar.PolicyID) *Template { + return p.templates[templateID] +} + +// AddTemplate inserts or updates a template with the given ID. +// Returns true if a template with the given ID did not already exist in the set. +func (p *PolicySet) AddTemplate(templateID cedar.PolicyID, template *Template) bool { + _, exists := p.templates[templateID] + p.templates[templateID] = template + return !exists +} + +// RemoveTemplate removes a template from the PolicySet. +// Returns true if a template with the given ID already existed in the set. +func (p *PolicySet) RemoveTemplate(templateID cedar.PolicyID) bool { + _, exists := p.templates[templateID] + delete(p.templates, templateID) + return exists } diff --git a/x/exp/templates/template.go b/x/exp/templates/template.go index 3648e688..dac8432f 100644 --- a/x/exp/templates/template.go +++ b/x/exp/templates/template.go @@ -1,99 +1 @@ package templates - -import ( - "bytes" - "fmt" - "github.com/cedar-policy/cedar-go" - "github.com/cedar-policy/cedar-go/internal/parser" - "github.com/cedar-policy/cedar-go/types" - "github.com/cedar-policy/cedar-go/x/exp/ast" -) - -// Template represents a Cedar policy template that can be linked with slot values -// to create concrete policies. It's a wrapper around the internal parser.Policy type. -type Template parser.Policy - -// MarshalCedar serializes the Template into its Cedar language representation. -// Returns the serialized template as a byte slice. -func (p *Template) MarshalCedar() []byte { - cedarPolicy := (*parser.Policy)(p) - - var buf bytes.Buffer - cedarPolicy.MarshalCedar(&buf) - - return buf.Bytes() -} - -// SetFilename sets the filename of this template. -// This is useful for error reporting and debugging purposes. -func (p *Template) SetFilename(fileName string) { - p.Position.Filename = fileName -} - -func (p *Template) Slots() []types.SlotID { - x := (*ast.Policy)(p) - - return x.Slots() -} - -// LinkedPolicy represents a template that has been linked with specific slot values. -// It's a wrapper around the internal parser.LinkedPolicy type. -//type LinkedPolicy parser.LinkedPolicy - -type LinkedPolicy struct { - templateID cedar.PolicyID - linkID cedar.PolicyID - slotEnv map[types.SlotID]types.EntityUID -} - -// LinkTemplate creates a LinkedPolicy by binding slot values to a template. -// Parameters: -// - template: The policy template to link -// - templateID: The identifier for the template -// - linkID: The identifier for the resulting linked policy -// - slotEnv: A map of slot IDs to entity UIDs that will be substituted into the template -// -// Returns a LinkedPolicy that can be rendered into a concrete Policy. -func (p *PolicySet) LinkTemplate(templateID cedar.PolicyID, linkID cedar.PolicyID, slotEnv map[types.SlotID]types.EntityUID) error { - template := p.GetTemplate(templateID) - if template == nil { - return fmt.Errorf("template %s not found", templateID) - } - - if len(slotEnv) < len(template.Slots()) { - return fmt.Errorf("template %s requires %d variables, slot env has %d", templateID, len(template.Slots()), len(slotEnv)) - } - - for _, slotID := range template.Slots() { - if _, ok := slotEnv[slotID]; !ok { - return fmt.Errorf("template %s requires variable %s, missing from slot env", templateID, slotID) - } - } - - link := LinkedPolicy{templateID, linkID, slotEnv} - p.links[linkID] = &link - - return nil -} - -// GetTemplate returns the Template with the given ID. -// If a template with the given ID does not exist, nil is returned. -func (p PolicySet) GetTemplate(templateID cedar.PolicyID) *Template { - return p.templates[templateID] -} - -// AddTemplate inserts or updates a template with the given ID. -// Returns true if a template with the given ID did not already exist in the set. -func (p *PolicySet) AddTemplate(templateID cedar.PolicyID, template *Template) bool { - _, exists := p.templates[templateID] - p.templates[templateID] = template - return !exists -} - -// RemoveTemplate removes a template from the PolicySet. -// Returns true if a template with the given ID already existed in the set. -func (p *PolicySet) RemoveTemplate(templateID cedar.PolicyID) bool { - _, exists := p.templates[templateID] - delete(p.templates, templateID) - return exists -} From 97dee92a86fa371262f634352fc8e46eeb30ae1e Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Sat, 7 Jun 2025 09:55:04 -0300 Subject: [PATCH 16/24] tests: fix templates tests Signed-off-by: Caio Ferreira --- x/exp/templates/policy_set.go | 18 ++ x/exp/templates/template_test.go | 355 ++++++++++++++++--------------- 2 files changed, 196 insertions(+), 177 deletions(-) diff --git a/x/exp/templates/policy_set.go b/x/exp/templates/policy_set.go index d3449a38..23e70792 100644 --- a/x/exp/templates/policy_set.go +++ b/x/exp/templates/policy_set.go @@ -75,6 +75,7 @@ func (p *PolicySet) Add(policyID cedar.PolicyID, policy *Policy) bool { return !exists } +// todo: check to see if it's a static policy or a linked policy // Remove removes a policy from the PolicySet. Returns true if a policy with // the given ID already existed in the set. func (p *PolicySet) Remove(policyID cedar.PolicyID) bool { @@ -221,6 +222,16 @@ type LinkedPolicy struct { slotEnv map[types.SlotID]types.EntityUID } +// TemplateID returns the PolicyID of the template associated with this LinkedPolicy. +func (l *LinkedPolicy) TemplateID() cedar.PolicyID { + return l.templateID +} + +// LinkID returns the PolicyID of this LinkedPolicy. +func (l *LinkedPolicy) LinkID() cedar.PolicyID { + return l.linkID +} + // LinkTemplate creates a LinkedPolicy by binding slot values to a template. // Parameters: // - template: The policy template to link @@ -251,6 +262,12 @@ func (p *PolicySet) LinkTemplate(templateID cedar.PolicyID, linkID cedar.PolicyI return nil } +// GetLinkedPolicy returns the LinkedPolicy associated with the given link ID. +// If the linked policy does not exist, it returns nil. +func (p *PolicySet) GetLinkedPolicy(linkID cedar.PolicyID) *LinkedPolicy { + return p.links[linkID] +} + // GetTemplate returns the Template with the given ID. // If a template with the given ID does not exist, nil is returned. func (p PolicySet) GetTemplate(templateID cedar.PolicyID) *Template { @@ -265,6 +282,7 @@ func (p *PolicySet) AddTemplate(templateID cedar.PolicyID, template *Template) b return !exists } +//todo: remove all linked policies that reference the template // RemoveTemplate removes a template from the PolicySet. // Returns true if a template with the given ID already existed in the set. func (p *PolicySet) RemoveTemplate(templateID cedar.PolicyID) bool { diff --git a/x/exp/templates/template_test.go b/x/exp/templates/template_test.go index b5860055..3c7fcaa6 100644 --- a/x/exp/templates/template_test.go +++ b/x/exp/templates/template_test.go @@ -1,179 +1,180 @@ package templates_test -//func TestPolicySetTemplateManagement(t *testing.T) { -// t.Run("template round-trip", func(t *testing.T) { -// policySet := cedar.NewPolicySet() -// -// var templateBody parser.Policy -// templateString := `@id("test_template") -//permit ( -// principal == ?principal, -// action, -// resource -//);` -// testutil.OK(t, templateBody.UnmarshalCedar([]byte(templateString))) -// template := templates.Template(templateBody) -// -// templateID := cedar.PolicyID("test_template_id") -// added := policySet.AddTemplate(templateID, &template) -// testutil.Equals(t, added, true) -// -// retrievedTemplate := policySet.GetTemplate(templateID) -// testutil.Equals(t, retrievedTemplate != nil, true) -// -// originalBytes := template.MarshalCedar() -// retrievedBytes := retrievedTemplate.MarshalCedar() -// testutil.Equals(t, string(retrievedBytes), string(originalBytes)) -// -// removed := policySet.RemoveTemplate(templateID) -// testutil.Equals(t, removed, true) -// -// retrievedTemplateAfterRemoval := policySet.GetTemplate(templateID) -// testutil.Equals(t, retrievedTemplateAfterRemoval, (*cedar.Template)(nil)) -// }) -// -// t.Run("remove non-existent template", func(t *testing.T) { -// policySet := cedar.NewPolicySet() -// templateID := cedar.PolicyID("non_existent_template") -// removed := policySet.RemoveTemplate(templateID) -// testutil.Equals(t, removed, false) -// }) -// -// t.Run("add template with existing ID", func(t *testing.T) { -// policySet := cedar.NewPolicySet() -// templateID := cedar.PolicyID("duplicate_template_id") -// -// var templateBody parser.Policy -// templateString := `@id("test_template") -//permit ( -// principal, -// action, -// resource -//);` -// testutil.OK(t, templateBody.UnmarshalCedar([]byte(templateString))) -// template := cedar.Template(templateBody) -// -// // First add should succeed -// isNew := policySet.AddTemplate(templateID, &template) -// testutil.Equals(t, isNew, true) -// -// // Second add with same ID should return false -// isNew = policySet.AddTemplate(templateID, &template) -// testutil.Equals(t, isNew, false) -// }) -//} -// -//func TestLinkTemplateToPolicy(t *testing.T) { -// linkTests := []struct { -// Name string -// TemplateString string -// TemplateID string -// LinkID string -// Env map[types.SlotID]types.EntityUID -// Want string -// }{ -// -// { -// "principal ScopeTypeEq", -// `@id("scope_eq_test") -//permit ( -// principal == ?principal, -// action, -// resource -//);`, -// "scope_eq_test", -// "scope_eq_link", -// map[types.SlotID]types.EntityUID{"?principal": types.NewEntityUID("User", "bob")}, -// `{"annotations":{"id":"scope_eq_test"},"effect":"permit","principal":{"op":"==","entity":{"type":"User","id":"bob"}},"action":{"op":"All"},"resource":{"op":"All"}}`, -// }, -// -// { -// "principal ScopeTypeIn", -// `@id("scope_in_test") -//permit ( -// principal in ?principal, -// action, -// resource -//);`, -// "scope_in_test", -// "scope_in_link", -// map[types.SlotID]types.EntityUID{"?principal": types.NewEntityUID("User", "charlie")}, -// `{"annotations":{"id":"scope_in_test"},"effect":"permit","principal":{"op":"in","entity":{"type":"User","id":"charlie"}},"action":{"op":"All"},"resource":{"op":"All"}}`, -// }, -// { -// "principal ScopeTypeIsIn", -// `@id("scope_isin_test") -//permit ( -// principal is User in ?principal, -// action, -// resource -//);`, -// "scope_isin_test", -// "scope_isin_link", -// map[types.SlotID]types.EntityUID{"?principal": types.NewEntityUID("User", "dave")}, -// `{"annotations":{"id":"scope_isin_test"},"effect":"permit","principal":{"op":"is","entity_type":"User","in":{"entity":{"type":"User","id":"dave"}}},"action":{"op":"All"},"resource":{"op":"All"}}`, -// }, -// { -// "resource ScopeTypeEq", -// `@id("resource_scope_eq_test") -//permit ( -// principal, -// action, -// resource == ?resource -//);`, -// "resource_scope_eq_test", -// "scope_eq_link", -// map[types.SlotID]types.EntityUID{"?resource": types.NewEntityUID("Album", "trip")}, -// `{"annotations":{"id":"resource_scope_eq_test"},"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"==","entity":{"type":"Album","id":"trip"}}}`, -// }, -// { -// "resource ScopeTypeIn", -// `@id("resource_scope_in_test") -//permit ( -// principal, -// action, -// resource in ?resource -//);`, -// "resource_scope_in_test", -// "scope_in_link", -// map[types.SlotID]types.EntityUID{"?resource": types.NewEntityUID("Album", "trip")}, -// `{"annotations":{"id":"resource_scope_in_test"},"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"in","entity":{"type":"Album","id":"trip"}}}`, -// }, -// { -// "resource ScopeTypeIsIn", -// `@id("resource_scope_isin_test") -//permit ( -// principal, -// action, -// resource is Album in ?resource -//);`, -// "resource_scope_isin_test", -// "scope_isin_link", -// map[types.SlotID]types.EntityUID{"?resource": types.NewEntityUID("Album", "trip")}, -// `{"annotations":{"id":"resource_scope_isin_test"},"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"is","entity_type":"Album","in":{"entity":{"type":"Album","id":"trip"}}}}`, -// }, -// } -// -// for _, tt := range linkTests { -// t.Run(tt.Name, func(t *testing.T) { -// t.Parallel() -// -// var templateBody parser.Policy -// testutil.OK(t, templateBody.UnmarshalCedar([]byte(tt.TemplateString))) -// template := cedar.Template(templateBody) -// -// linkedPolicy := cedar.LinkTemplate(template, tt.TemplateID, tt.LinkID, tt.Env) -// -// testutil.Equals(t, linkedPolicy.LinkID, tt.LinkID) -// testutil.Equals(t, linkedPolicy.TemplateID, tt.TemplateID) -// -// policy, err := linkedPolicy.Render() -// testutil.OK(t, err) -// -// pj, err := policy.MarshalJSON() -// testutil.OK(t, err) -// -// testutil.Equals(t, string(pj), tt.Want) -// }) -// } -//} +import ( + "github.com/cedar-policy/cedar-go" + "github.com/cedar-policy/cedar-go/internal/parser" + "github.com/cedar-policy/cedar-go/internal/testutil" + "github.com/cedar-policy/cedar-go/types" + "github.com/cedar-policy/cedar-go/x/exp/templates" + "testing" +) + +func TestPolicySetTemplateManagement(t *testing.T) { + t.Run("template round-trip", func(t *testing.T) { + policySet := templates.NewPolicySet() + + var templateBody parser.Policy + templateString := `@id("test_template") +permit ( + principal == ?principal, + action, + resource +);` + testutil.OK(t, templateBody.UnmarshalCedar([]byte(templateString))) + template := templates.Template(templateBody) + + templateID := cedar.PolicyID("test_template_id") + added := policySet.AddTemplate(templateID, &template) + testutil.Equals(t, added, true) + + retrievedTemplate := policySet.GetTemplate(templateID) + testutil.Equals(t, retrievedTemplate != nil, true) + + originalBytes := template.MarshalCedar() + retrievedBytes := retrievedTemplate.MarshalCedar() + testutil.Equals(t, string(retrievedBytes), string(originalBytes)) + + removed := policySet.RemoveTemplate(templateID) + testutil.Equals(t, removed, true) + + retrievedTemplateAfterRemoval := policySet.GetTemplate(templateID) + testutil.Equals(t, retrievedTemplateAfterRemoval, (*templates.Template)(nil)) + }) + + t.Run("remove non-existent template", func(t *testing.T) { + policySet := templates.NewPolicySet() + templateID := cedar.PolicyID("non_existent_template") + removed := policySet.RemoveTemplate(templateID) + testutil.Equals(t, removed, false) + }) + + t.Run("add template with existing ID", func(t *testing.T) { + policySet := templates.NewPolicySet() + templateID := cedar.PolicyID("duplicate_template_id") + + var templateBody parser.Policy + templateString := `@id("test_template") +permit ( + principal, + action, + resource +);` + testutil.OK(t, templateBody.UnmarshalCedar([]byte(templateString))) + template := templates.Template(templateBody) + + // First add should succeed + isNew := policySet.AddTemplate(templateID, &template) + testutil.Equals(t, isNew, true) + + // Second add with same ID should return false + isNew = policySet.AddTemplate(templateID, &template) + testutil.Equals(t, isNew, false) + }) +} + +func TestLinkTemplateToPolicy(t *testing.T) { + linkTests := []struct { + Name string + TemplateString string + LinkID cedar.PolicyID + Env map[types.SlotID]types.EntityUID + Want string + }{ + { + "principal ScopeTypeEq", + `permit ( + principal == ?principal, + action, + resource +);`, + "scope_eq_link", + map[types.SlotID]types.EntityUID{"?principal": types.NewEntityUID("User", "bob")}, + `{"effect":"permit","principal":{"op":"==","entity":{"type":"User","id":"bob"}},"action":{"op":"All"},"resource":{"op":"All"}}`, + }, + { + "principal ScopeTypeIn", + `permit ( + principal in ?principal, + action, + resource +);`, + "scope_in_link", + map[types.SlotID]types.EntityUID{"?principal": types.NewEntityUID("User", "charlie")}, + `{"effect":"permit","principal":{"op":"in","entity":{"type":"User","id":"charlie"}},"action":{"op":"All"},"resource":{"op":"All"}}`, + }, + { + "principal ScopeTypeIsIn", + `permit ( + principal is User in ?principal, + action, + resource +);`, + "scope_isin_link", + map[types.SlotID]types.EntityUID{"?principal": types.NewEntityUID("User", "dave")}, + `{"effect":"permit","principal":{"op":"is","entity_type":"User","in":{"entity":{"type":"User","id":"dave"}}},"action":{"op":"All"},"resource":{"op":"All"}}`, + }, + { + "resource ScopeTypeEq", + `permit ( + principal, + action, + resource == ?resource +);`, + "scope_eq_link", + map[types.SlotID]types.EntityUID{"?resource": types.NewEntityUID("Album", "trip")}, + `{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"==","entity":{"type":"Album","id":"trip"}}}`, + }, + { + "resource ScopeTypeIn", + `permit ( + principal, + action, + resource in ?resource +);`, + "scope_in_link", + map[types.SlotID]types.EntityUID{"?resource": types.NewEntityUID("Album", "trip")}, + `{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"in","entity":{"type":"Album","id":"trip"}}}`, + }, + { + "resource ScopeTypeIsIn", + `permit ( + principal, + action, + resource is Album in ?resource +);`, + "scope_isin_link", + map[types.SlotID]types.EntityUID{"?resource": types.NewEntityUID("Album", "trip")}, + `{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"is","entity_type":"Album","in":{"entity":{"type":"Album","id":"trip"}}}}`, + }, + } + + for _, tt := range linkTests { + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + + policySet, err := templates.NewPolicySetFromBytes("test.cedar", []byte(tt.TemplateString)) + testutil.OK(t, err) + + templateID := cedar.PolicyID("template0") + + err = policySet.LinkTemplate(templateID, tt.LinkID, tt.Env) + testutil.OK(t, err) + + linkedPolicy := policySet.GetLinkedPolicy(tt.LinkID) + + testutil.Equals(t, linkedPolicy.LinkID(), tt.LinkID) + testutil.Equals(t, linkedPolicy.TemplateID(), templateID) + + for policyID, policy := range policySet.All() { + if policyID == tt.LinkID { + pj, err := policy.MarshalJSON() + testutil.OK(t, err) + + testutil.Equals(t, string(pj), tt.Want) + + break + } + } + }) + } +} From 6eccb2615050bbff954c47f051cd775180950d54 Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Sat, 7 Jun 2025 10:03:12 -0300 Subject: [PATCH 17/24] feat: remove linked policies once template is removed Signed-off-by: Caio Ferreira --- x/exp/templates/policy_set.go | 10 +++++++++- x/exp/templates/template_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/x/exp/templates/policy_set.go b/x/exp/templates/policy_set.go index 23e70792..3a8b710f 100644 --- a/x/exp/templates/policy_set.go +++ b/x/exp/templates/policy_set.go @@ -282,11 +282,19 @@ func (p *PolicySet) AddTemplate(templateID cedar.PolicyID, template *Template) b return !exists } -//todo: remove all linked policies that reference the template // RemoveTemplate removes a template from the PolicySet. // Returns true if a template with the given ID already existed in the set. func (p *PolicySet) RemoveTemplate(templateID cedar.PolicyID) bool { _, exists := p.templates[templateID] + if exists { + // Remove all linked policies that reference this template + for linkID, link := range p.links { + if link.templateID == templateID { + delete(p.links, linkID) + } + } + } + delete(p.templates, templateID) return exists } diff --git a/x/exp/templates/template_test.go b/x/exp/templates/template_test.go index 3c7fcaa6..d33116d7 100644 --- a/x/exp/templates/template_test.go +++ b/x/exp/templates/template_test.go @@ -70,6 +70,38 @@ permit ( isNew = policySet.AddTemplate(templateID, &template) testutil.Equals(t, isNew, false) }) + + t.Run("removing template removes linked policies", func(t *testing.T) { + templateString := `permit ( + principal == ?principal, + action, + resource +);` + templateID := cedar.PolicyID("template0") + + policySet, err := templates.NewPolicySetFromBytes("test.cedar", []byte(templateString)) + testutil.OK(t, err) + + // Link a policy to the template + linkID := cedar.PolicyID("linked_policy_id") + env := map[types.SlotID]types.EntityUID{ + "?principal": types.NewEntityUID("User", "alice"), + } + err = policySet.LinkTemplate(templateID, linkID, env) + testutil.OK(t, err) + + // Ensure the linked policy exists + linkedPolicy := policySet.GetLinkedPolicy(linkID) + testutil.Equals(t, linkedPolicy != nil, true) + + // Remove the template + removed := policySet.RemoveTemplate(templateID) + testutil.Equals(t, removed, true) + + // The linked policy should also be removed + linkedPolicyAfterRemoval := policySet.GetLinkedPolicy(linkID) + testutil.Equals(t, linkedPolicyAfterRemoval == nil, true) + }) } func TestLinkTemplateToPolicy(t *testing.T) { From 39bdf0e64883696a7d64a2ee94db71a266057888 Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Sat, 7 Jun 2025 10:10:11 -0300 Subject: [PATCH 18/24] feat: add support to remove links and ensure that link ids are unique Signed-off-by: Caio Ferreira --- x/exp/templates/policy_set.go | 64 ++++++++++++++++++-------------- x/exp/templates/template_test.go | 59 +++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 28 deletions(-) diff --git a/x/exp/templates/policy_set.go b/x/exp/templates/policy_set.go index 3a8b710f..195712b9 100644 --- a/x/exp/templates/policy_set.go +++ b/x/exp/templates/policy_set.go @@ -21,18 +21,18 @@ type PolicyMap map[cedar.PolicyID]*Policy // PolicySet is a set of named policies against which a request can be authorized. type PolicySet struct { // policies are stored internally so we can handle performance, concurrency bookkeeping however we want - policies PolicyMap + staticPolicies PolicyMap + linkedPolicies map[cedar.PolicyID]*LinkedPolicy templates map[cedar.PolicyID]*Template - links map[cedar.PolicyID]*LinkedPolicy } // NewPolicySet creates a new, empty PolicySet func NewPolicySet() *PolicySet { return &PolicySet{ - policies: PolicyMap{}, - templates: make(map[cedar.PolicyID]*Template), - links: make(map[cedar.PolicyID]*LinkedPolicy), + staticPolicies: PolicyMap{}, + templates: make(map[cedar.PolicyID]*Template), + linkedPolicies: make(map[cedar.PolicyID]*LinkedPolicy), } } @@ -58,44 +58,47 @@ func NewPolicySetFromBytes(fileName string, document []byte) (*PolicySet, error) templateMap[policyID] = p } - return &PolicySet{policies: policyMap, templates: templateMap, links: make(map[cedar.PolicyID]*LinkedPolicy)}, nil + return &PolicySet{staticPolicies: policyMap, templates: templateMap, linkedPolicies: make(map[cedar.PolicyID]*LinkedPolicy)}, nil } // Get returns the Policy with the given ID. If a policy with the given ID // does not exist, nil is returned. func (p *PolicySet) Get(policyID cedar.PolicyID) *Policy { - return p.policies[policyID] + return p.staticPolicies[policyID] } // Add inserts or updates a policy with the given ID. Returns true if a policy // with the given ID did not already exist in the set. func (p *PolicySet) Add(policyID cedar.PolicyID, policy *Policy) bool { - _, exists := p.policies[policyID] - p.policies[policyID] = policy + _, exists := p.staticPolicies[policyID] + p.staticPolicies[policyID] = policy return !exists } -// todo: check to see if it's a static policy or a linked policy // Remove removes a policy from the PolicySet. Returns true if a policy with // the given ID already existed in the set. func (p *PolicySet) Remove(policyID cedar.PolicyID) bool { - _, exists := p.policies[policyID] - delete(p.policies, policyID) - return exists + _, staticExists := p.staticPolicies[policyID] + delete(p.staticPolicies, policyID) + + _, linkExists := p.linkedPolicies[policyID] + delete(p.linkedPolicies, policyID) + + return staticExists || linkExists } // Map returns a new PolicyMap instance of the policies in the PolicySet. // // Deprecated: use the iterator returned by All() like so: maps.Collect(ps.All()) func (p *PolicySet) Map() PolicyMap { - return maps.Clone(p.policies) + return maps.Clone(p.staticPolicies) } // MarshalCedar emits a concatenated Cedar representation of a PolicySet. The policy names are stripped, but policies // are emitted in lexicographical order by ID. func (p *PolicySet) MarshalCedar() []byte { - ids := make([]cedar.PolicyID, 0, len(p.policies)) - for k := range p.policies { + ids := make([]cedar.PolicyID, 0, len(p.staticPolicies)) + for k := range p.staticPolicies { ids = append(ids, k) } slices.Sort(ids) @@ -103,10 +106,10 @@ func (p *PolicySet) MarshalCedar() []byte { var buf bytes.Buffer i := 0 for _, id := range ids { - policy := p.policies[id] + policy := p.staticPolicies[id] buf.Write(policy.MarshalCedar()) - if i < len(p.policies)-1 { + if i < len(p.staticPolicies)-1 { buf.WriteString("\n\n") } i++ @@ -119,9 +122,9 @@ func (p *PolicySet) MarshalCedar() []byte { // [Cedar documentation]: https://docs.cedarpolicy.com/policies/json-format.html func (p *PolicySet) MarshalJSON() ([]byte, error) { jsonPolicySet := internaljson.PolicySetJSON{ - StaticPolicies: make(internaljson.PolicySet, len(p.policies)), + StaticPolicies: make(internaljson.PolicySet, len(p.staticPolicies)), } - for k, v := range p.policies { + for k, v := range p.staticPolicies { jsonPolicySet.StaticPolicies[string(k)] = (*internaljson.Policy)(v.AST()) } return json.Marshal(jsonPolicySet) @@ -136,10 +139,10 @@ func (p *PolicySet) UnmarshalJSON(b []byte) error { return err } *p = PolicySet{ - policies: make(PolicyMap, len(jsonPolicySet.StaticPolicies)), + staticPolicies: make(PolicyMap, len(jsonPolicySet.StaticPolicies)), } for k, v := range jsonPolicySet.StaticPolicies { - p.policies[cedar.PolicyID(k)] = newPolicy((*internalast.Policy)(v)) // NewPolicyFromAST((*ast.Policy)(v)) + p.staticPolicies[cedar.PolicyID(k)] = newPolicy((*internalast.Policy)(v)) // NewPolicyFromAST((*ast.Policy)(v)) } return nil } @@ -147,13 +150,13 @@ func (p *PolicySet) UnmarshalJSON(b []byte) error { // All returns an iterator over the (PolicyID, *Policy) tuples in the PolicySet func (p *PolicySet) All() iter.Seq2[cedar.PolicyID, *Policy] { return func(yield func(cedar.PolicyID, *Policy) bool) { - for k, v := range p.policies { + for k, v := range p.staticPolicies { if !yield(k, v) { break } } - for k, v := range p.links { + for k, v := range p.linkedPolicies { // Render links on read to make template changes propagate policy, err := p.render(*v) if err != nil { //todo: think how to propagate this error @@ -241,6 +244,11 @@ func (l *LinkedPolicy) LinkID() cedar.PolicyID { // // Returns a LinkedPolicy that can be rendered into a concrete Policy. func (p *PolicySet) LinkTemplate(templateID cedar.PolicyID, linkID cedar.PolicyID, slotEnv map[types.SlotID]types.EntityUID) error { + _, exists := p.staticPolicies[linkID] + if exists { + return fmt.Errorf("link ID %s already exists in the policy set", linkID) + } + template := p.GetTemplate(templateID) if template == nil { return fmt.Errorf("template %s not found", templateID) @@ -257,7 +265,7 @@ func (p *PolicySet) LinkTemplate(templateID cedar.PolicyID, linkID cedar.PolicyI } link := LinkedPolicy{templateID, linkID, slotEnv} - p.links[linkID] = &link + p.linkedPolicies[linkID] = &link return nil } @@ -265,7 +273,7 @@ func (p *PolicySet) LinkTemplate(templateID cedar.PolicyID, linkID cedar.PolicyI // GetLinkedPolicy returns the LinkedPolicy associated with the given link ID. // If the linked policy does not exist, it returns nil. func (p *PolicySet) GetLinkedPolicy(linkID cedar.PolicyID) *LinkedPolicy { - return p.links[linkID] + return p.linkedPolicies[linkID] } // GetTemplate returns the Template with the given ID. @@ -288,9 +296,9 @@ func (p *PolicySet) RemoveTemplate(templateID cedar.PolicyID) bool { _, exists := p.templates[templateID] if exists { // Remove all linked policies that reference this template - for linkID, link := range p.links { + for linkID, link := range p.linkedPolicies { if link.templateID == templateID { - delete(p.links, linkID) + delete(p.linkedPolicies, linkID) } } } diff --git a/x/exp/templates/template_test.go b/x/exp/templates/template_test.go index d33116d7..f13ee992 100644 --- a/x/exp/templates/template_test.go +++ b/x/exp/templates/template_test.go @@ -71,6 +71,33 @@ permit ( testutil.Equals(t, isNew, false) }) + t.Run("cannot use link id already used by static policy", func(t *testing.T) { + templateString := `permit ( + principal == ?principal, + action, + resource +); + +permit ( + principal, + action, + resource +);` + templateID := cedar.PolicyID("template0") + policyID := cedar.PolicyID("policy0") + + policySet, err := templates.NewPolicySetFromBytes("test.cedar", []byte(templateString)) + testutil.OK(t, err) + + // Link a policy to the template + //linkID := cedar.PolicyID("linked_policy_id") + env := map[types.SlotID]types.EntityUID{ + "?principal": types.NewEntityUID("User", "alice"), + } + err = policySet.LinkTemplate(templateID, policyID, env) + testutil.Error(t, err) + }) + t.Run("removing template removes linked policies", func(t *testing.T) { templateString := `permit ( principal == ?principal, @@ -102,6 +129,38 @@ permit ( linkedPolicyAfterRemoval := policySet.GetLinkedPolicy(linkID) testutil.Equals(t, linkedPolicyAfterRemoval == nil, true) }) + + t.Run("remove method can also remove linked policy", func(t *testing.T) { + templateString := `permit ( + principal == ?principal, + action, + resource +);` + templateID := cedar.PolicyID("template0") + + policySet, err := templates.NewPolicySetFromBytes("test.cedar", []byte(templateString)) + testutil.OK(t, err) + + // Link a policy to the template + linkID := cedar.PolicyID("linked_policy_id") + env := map[types.SlotID]types.EntityUID{ + "?principal": types.NewEntityUID("User", "alice"), + } + err = policySet.LinkTemplate(templateID, linkID, env) + testutil.OK(t, err) + + // Ensure the linked policy exists + linkedPolicy := policySet.GetLinkedPolicy(linkID) + testutil.Equals(t, linkedPolicy != nil, true) + + // Remove the template + removed := policySet.Remove(linkID) + testutil.Equals(t, removed, true) + + // The linked policy should also be removed + linkedPolicyAfterRemoval := policySet.GetLinkedPolicy(linkID) + testutil.Equals(t, linkedPolicyAfterRemoval == nil, true) + }) } func TestLinkTemplateToPolicy(t *testing.T) { From eb2f7d2652a7a6da2cfc2a64b9ab6258061b65c2 Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Sat, 7 Jun 2025 10:17:56 -0300 Subject: [PATCH 19/24] fix: original tests Signed-off-by: Caio Ferreira --- policy_set_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/policy_set_test.go b/policy_set_test.go index 41ef6014..26a89b46 100644 --- a/policy_set_test.go +++ b/policy_set_test.go @@ -126,7 +126,7 @@ forbid ( testutil.OK(t, err) ps := cedar.NewPolicySet() - for i, p := range policies.StaticPolicies { + for i, p := range policies { p.SetFilename("example.cedar") ps.Add(cedar.PolicyID(fmt.Sprintf("policy%d", i)), p) } @@ -142,8 +142,8 @@ func TestPolicySetMap(t *testing.T) { t.Parallel() ps, err := cedar.NewPolicySetFromBytes("", []byte(`permit (principal, action, resource);`)) testutil.OK(t, err) - m := ps.Map() - testutil.Equals(t, len(m.StaticPolicies), 1) + m := maps.Collect(ps.All()) + testutil.Equals(t, len(m), 1) } func TestPolicySetJSON(t *testing.T) { @@ -159,7 +159,7 @@ func TestPolicySetJSON(t *testing.T) { var ps cedar.PolicySet err := ps.UnmarshalJSON([]byte(`{"staticPolicies":{"policy0":{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"}}}}`)) testutil.OK(t, err) - testutil.Equals(t, len(ps.Map().StaticPolicies), 1) + testutil.Equals(t, len(maps.Collect(ps.All())), 1) }) t.Run("MarshalOK", func(t *testing.T) { From ae8069f023a75dfb77719bd8d2f5388cf17f14a6 Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Sat, 7 Jun 2025 11:29:41 -0300 Subject: [PATCH 20/24] feat: add all marshal/unmarshal methods to template Signed-off-by: Caio Ferreira --- internal/json/json.go | 2 +- internal/json/json_marshal.go | 2 + internal/json/json_unmarshal.go | 10 +- x/exp/templates/policy.go | 77 ++++++ x/exp/templates/policy_set.go | 44 ++-- .../{template_test.go => policy_set_test.go} | 225 +++++++++++++++++- x/exp/templates/policy_test.go | 158 ++++++++++++ x/exp/templates/template.go | 1 - 8 files changed, 483 insertions(+), 36 deletions(-) rename x/exp/templates/{template_test.go => policy_set_test.go} (57%) create mode 100644 x/exp/templates/policy_test.go delete mode 100644 x/exp/templates/template.go diff --git a/internal/json/json.go b/internal/json/json.go index fc80f849..9a1c0e73 100644 --- a/internal/json/json.go +++ b/internal/json/json.go @@ -12,7 +12,7 @@ type policyJSON struct { Principal scopeJSON `json:"principal"` Action scopeJSON `json:"action"` Resource scopeJSON `json:"resource"` - Conditions []conditionJSON `json:"conditions,omitempty"` + Conditions []conditionJSON `json:"conditions"` // [Cedar documentation]: https://docs.cedarpolicy.com/policies/json-format.html#policy-set-format } // scopeInJSON uses the implicit form of EntityUID JSON serialization to match the Rust SDK diff --git a/internal/json/json_marshal.go b/internal/json/json_marshal.go index 7ff9dccc..ed614cf6 100644 --- a/internal/json/json_marshal.go +++ b/internal/json/json_marshal.go @@ -341,6 +341,8 @@ func (p *Policy) MarshalJSON() ([]byte, error) { j.Principal.FromNode(p.Principal) j.Action.FromNode(p.Action) j.Resource.FromNode(p.Resource) + + j.Conditions = make([]conditionJSON, 0, len(p.Conditions)) // [Cedar documentation]: https://docs.cedarpolicy.com/policies/json-format.html#policy-set-format for _, c := range p.Conditions { var cond conditionJSON cond.Kind = "when" diff --git a/internal/json/json_unmarshal.go b/internal/json/json_unmarshal.go index 7d3cea5b..4a89dd29 100644 --- a/internal/json/json_unmarshal.go +++ b/internal/json/json_unmarshal.go @@ -87,9 +87,9 @@ func scopeInEntityReference(s *scopeInJSON) (types.EntityReference, error) { } func isSlotValid(entRef types.EntityReference, slot types.SlotID) bool { - switch entRef.(type) { + switch v := entRef.(type) { case types.SlotID: - return slot == types.PrincipalSlot + return v == slot default: return true } @@ -160,7 +160,7 @@ func (s *scopeJSON) ToResourceNode(policy *Policy, allowedSlot types.SlotID) err } if !isSlotValid(ref, allowedSlot) { - return fmt.Errorf("variable used in principal slot is not %s", allowedSlot) + return fmt.Errorf("variable used in resource slot is not %s", allowedSlot) } policy.unwrap().ResourceEq(ref) @@ -173,7 +173,7 @@ func (s *scopeJSON) ToResourceNode(policy *Policy, allowedSlot types.SlotID) err } if !isSlotValid(ref, allowedSlot) { - return fmt.Errorf("variable used in principal slot is not %s", allowedSlot) + return fmt.Errorf("variable used in resource slot is not %s", allowedSlot) } policy.unwrap().ResourceIn(ref) @@ -192,7 +192,7 @@ func (s *scopeJSON) ToResourceNode(policy *Policy, allowedSlot types.SlotID) err } if !isSlotValid(ref, allowedSlot) { - return fmt.Errorf("variable used in principal slot is not %s", allowedSlot) + return fmt.Errorf("variable used in resource slot is not %s", allowedSlot) } policy.unwrap().ResourceIsIn(types.EntityType(s.EntityType), ref) diff --git a/x/exp/templates/policy.go b/x/exp/templates/policy.go index 483edf41..06a1a2e9 100644 --- a/x/exp/templates/policy.go +++ b/x/exp/templates/policy.go @@ -3,6 +3,7 @@ package templates import ( "bytes" "github.com/cedar-policy/cedar-go" + "github.com/cedar-policy/cedar-go/types" "github.com/cedar-policy/cedar-go/ast" "github.com/cedar-policy/cedar-go/internal/eval" @@ -102,3 +103,79 @@ func (p *Policy) SetFilename(fileName string) { func (p *Policy) AST() *ast.Policy { return (*ast.Policy)(p.ast) } + +// We use parser.Policy as the underlying type for Template because +// a templates.Policy or cedar.Policy would be compiled, however, a Template +// is not compilable until it is linked with slot values to create a concrete policy. + +// Template represents a Cedar policy template that can be linked with slot values +// to create concrete policies. It's a wrapper around the internal parser.Policy type. +type Template parser.Policy + +func newTemplate(astIn *internalast.Policy) *Template { + t := (*Template)(astIn) + return t +} + +// MarshalCedar serializes the Template into its Cedar language representation. +// Returns the serialized template as a byte slice. +func (p *Template) MarshalCedar() []byte { + cedarPolicy := (*parser.Policy)(p) + + var buf bytes.Buffer + cedarPolicy.MarshalCedar(&buf) + + return buf.Bytes() +} + +func (p *Template) UnmarshalCedar(b []byte) error { + var cedarPolicy parser.Policy + if err := cedarPolicy.UnmarshalCedar(b); err != nil { + return err + } + + *p = *newTemplate((*internalast.Policy)(&cedarPolicy)) + + return nil +} + +// 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 *Template) MarshalJSON() ([]byte, error) { + policyAST := (*internalast.Policy)(p) + jsonPolicy := (*json.Policy)(policyAST) + + 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 *Template) UnmarshalJSON(b []byte) error { + var jsonPolicy json.Policy + if err := jsonPolicy.UnmarshalJSON(b); err != nil { + return err + } + + *p = *newTemplate((*internalast.Policy)(&jsonPolicy)) + + return nil +} + +// SetFilename sets the filename of this template. +// This is useful for error reporting and debugging purposes. +func (p *Template) SetFilename(fileName string) { + p.Position.Filename = fileName +} + +func (p *Template) Slots() []types.SlotID { + policyAST := (*internalast.Policy)(p) + return policyAST.Slots() +} + +// AST retrieves the AST of this Template. Do not modify the AST. +func (p *Template) AST() *ast.Policy { + policyAST := (*internalast.Policy)(p) + return (*ast.Policy)(policyAST) +} diff --git a/x/exp/templates/policy_set.go b/x/exp/templates/policy_set.go index 195712b9..d1000b84 100644 --- a/x/exp/templates/policy_set.go +++ b/x/exp/templates/policy_set.go @@ -18,6 +18,11 @@ import ( type PolicyMap map[cedar.PolicyID]*Policy +// All returns an iterator over the policy IDs and policies in the PolicyMap. +func (p PolicyMap) All() iter.Seq2[cedar.PolicyID, *Policy] { + return maps.All(p) +} + // PolicySet is a set of named policies against which a request can be authorized. type PolicySet struct { // policies are stored internally so we can handle performance, concurrency bookkeeping however we want @@ -117,16 +122,22 @@ func (p *PolicySet) MarshalCedar() []byte { return buf.Bytes() } +//todo: marshal links // MarshalJSON encodes a PolicySet in the JSON format specified by the [Cedar documentation]. // // [Cedar documentation]: https://docs.cedarpolicy.com/policies/json-format.html func (p *PolicySet) MarshalJSON() ([]byte, error) { jsonPolicySet := internaljson.PolicySetJSON{ StaticPolicies: make(internaljson.PolicySet, len(p.staticPolicies)), + Templates: make(internaljson.TemplateSet, len(p.templates)), } for k, v := range p.staticPolicies { jsonPolicySet.StaticPolicies[string(k)] = (*internaljson.Policy)(v.AST()) } + for k, v := range p.templates { + jsonPolicySet.Templates[string(k)] = (*internaljson.Policy)(v.AST()) + } + return json.Marshal(jsonPolicySet) } @@ -140,9 +151,13 @@ func (p *PolicySet) UnmarshalJSON(b []byte) error { } *p = PolicySet{ staticPolicies: make(PolicyMap, len(jsonPolicySet.StaticPolicies)), + templates: make(map[cedar.PolicyID]*Template, len(jsonPolicySet.Templates)), } for k, v := range jsonPolicySet.StaticPolicies { - p.staticPolicies[cedar.PolicyID(k)] = newPolicy((*internalast.Policy)(v)) // NewPolicyFromAST((*ast.Policy)(v)) + p.staticPolicies[cedar.PolicyID(k)] = newPolicy((*internalast.Policy)(v)) + } + for k, v := range jsonPolicySet.Templates { + p.templates[cedar.PolicyID(k)] = newTemplate((*internalast.Policy)(v)) } return nil } @@ -188,33 +203,6 @@ func (p *PolicySet) render(link LinkedPolicy) (*Policy, error) { return newPolicy(&astPolicy), nil } -// Template represents a Cedar policy template that can be linked with slot values -// to create concrete policies. It's a wrapper around the internal parser.Policy type. -type Template parser.Policy - -// MarshalCedar serializes the Template into its Cedar language representation. -// Returns the serialized template as a byte slice. -func (p *Template) MarshalCedar() []byte { - cedarPolicy := (*parser.Policy)(p) - - var buf bytes.Buffer - cedarPolicy.MarshalCedar(&buf) - - return buf.Bytes() -} - -// SetFilename sets the filename of this template. -// This is useful for error reporting and debugging purposes. -func (p *Template) SetFilename(fileName string) { - p.Position.Filename = fileName -} - -func (p *Template) Slots() []types.SlotID { - x := (*internalast.Policy)(p) - - return x.Slots() -} - // LinkedPolicy represents a template that has been linked with specific slot values. // It's a wrapper around the internal parser.LinkedPolicy type. //type LinkedPolicy parser.LinkedPolicy diff --git a/x/exp/templates/template_test.go b/x/exp/templates/policy_set_test.go similarity index 57% rename from x/exp/templates/template_test.go rename to x/exp/templates/policy_set_test.go index f13ee992..c3632c11 100644 --- a/x/exp/templates/template_test.go +++ b/x/exp/templates/policy_set_test.go @@ -1,14 +1,237 @@ package templates_test import ( + "fmt" + "maps" + "testing" + "github.com/cedar-policy/cedar-go" + "github.com/cedar-policy/cedar-go/ast" "github.com/cedar-policy/cedar-go/internal/parser" "github.com/cedar-policy/cedar-go/internal/testutil" "github.com/cedar-policy/cedar-go/types" "github.com/cedar-policy/cedar-go/x/exp/templates" - "testing" ) +func TestPolicyMap(t *testing.T) { + t.Parallel() + t.Run("All", func(t *testing.T) { + t.Parallel() + pm := templates.PolicyMap{ + "foo": templates.NewPolicyFromAST(ast.Permit()), + "bar": templates.NewPolicyFromAST(ast.Permit()), + } + + got := maps.Collect(pm.All()) + testutil.Equals(t, got, pm) + }) +} + +func TestNewPolicySetFromFile(t *testing.T) { + t.Parallel() + t.Run("err-in-tokenize", func(t *testing.T) { + t.Parallel() + _, err := templates.NewPolicySetFromBytes("policy.cedar", []byte(`"`)) + testutil.Error(t, err) + }) + t.Run("err-in-parse", func(t *testing.T) { + t.Parallel() + _, err := templates.NewPolicySetFromBytes("policy.cedar", []byte(`err`)) + testutil.Error(t, err) + }) + t.Run("annotations", func(t *testing.T) { + t.Parallel() + ps, err := templates.NewPolicySetFromBytes("policy.cedar", []byte(`@key("value") permit (principal, action, resource);`)) + testutil.OK(t, err) + testutil.Equals(t, ps.Get("policy0").Annotations(), cedar.Annotations{"key": "value"}) + }) +} + +func TestUpsertPolicy(t *testing.T) { + t.Parallel() + t.Run("insert", func(t *testing.T) { + t.Parallel() + + policy0 := templates.NewPolicyFromAST(ast.Forbid()) + + var policy1 templates.Policy + testutil.OK(t, policy1.UnmarshalJSON( + []byte(`{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"}}`), + )) + + ps := templates.NewPolicySet() + added := ps.Add("policy0", policy0) + testutil.Equals(t, added, true) + added = ps.Add("policy1", &policy1) + testutil.Equals(t, added, true) + + testutil.Equals(t, ps.Get("policy0"), policy0) + testutil.Equals(t, ps.Get("policy1"), &policy1) + testutil.Equals(t, ps.Get("policy2"), nil) + }) + t.Run("upsert", func(t *testing.T) { + t.Parallel() + + ps := templates.NewPolicySet() + + p1 := templates.NewPolicyFromAST(ast.Forbid()) + ps.Add("a wavering policy", p1) + + p2 := templates.NewPolicyFromAST(ast.Permit()) + added := ps.Add("a wavering policy", p2) + testutil.Equals(t, added, false) + + testutil.Equals(t, ps.Get("a wavering policy"), p2) + }) +} + +func TestDeletePolicy(t *testing.T) { + t.Parallel() + t.Run("delete non-existent", func(t *testing.T) { + t.Parallel() + + ps := templates.NewPolicySet() + + existed := ps.Remove("not a policy") + testutil.Equals(t, existed, false) + }) + t.Run("delete existing", func(t *testing.T) { + t.Parallel() + + ps := templates.NewPolicySet() + + p1 := templates.NewPolicyFromAST(ast.Forbid()) + ps.Add("a policy", p1) + existed := ps.Remove("a policy") + testutil.Equals(t, existed, true) + + testutil.Equals(t, ps.Get("a policy"), nil) + }) +} + +func TestNewPolicySetFromSlice(t *testing.T) { + t.Parallel() + + policiesStr := `permit ( + principal, + action == Action::"editPhoto", + resource +) +when { resource.owner == principal }; + +forbid ( + principal in Groups::"bannedUsers", + action, + resource +);` + + policies, err := templates.NewPolicyListFromBytes("", []byte(policiesStr)) + testutil.OK(t, err) + + ps := templates.NewPolicySet() + for i, p := range policies.StaticPolicies { + p.SetFilename("example.cedar") + ps.Add(cedar.PolicyID(fmt.Sprintf("policy%d", i)), p) + } + + testutil.Equals(t, ps.Get("policy0").Effect(), cedar.Permit) + testutil.Equals(t, ps.Get("policy1").Effect(), cedar.Forbid) + + testutil.Equals(t, string(ps.MarshalCedar()), policiesStr) + +} + +func TestPolicySetMap(t *testing.T) { + t.Parallel() + ps, err := templates.NewPolicySetFromBytes("", []byte(`permit (principal, action, resource);`)) + testutil.OK(t, err) + m := maps.Collect(ps.All()) + testutil.Equals(t, len(m), 1) +} + +func TestPolicySetJSON(t *testing.T) { + t.Parallel() + t.Run("UnmarshalError", func(t *testing.T) { + t.Parallel() + var ps templates.PolicySet + err := ps.UnmarshalJSON([]byte(`!@#$`)) + testutil.Error(t, err) + }) + + t.Run("UnmarshalOK", func(t *testing.T) { + t.Parallel() + var ps templates.PolicySet + err := ps.UnmarshalJSON([]byte(`{"staticPolicies":{"policy0":{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"}}},"templates":{"template0":{"effect":"permit","principal":{"op":"==","slot":"?principal"},"action":{"op":"All"},"resource":{"op":"All"}}}}`)) + testutil.OK(t, err) + testutil.Equals(t, len(maps.Collect(ps.All())), 1) + testutil.Equals(t, ps.GetTemplate("template0") != nil, true) + }) + + t.Run("MarshalOK", func(t *testing.T) { + t.Parallel() + ps, err := templates.NewPolicySetFromBytes("", []byte(`permit (principal, action, resource); + +permit (principal == ?principal, action, resource);`)) + testutil.OK(t, err) + out, err := ps.MarshalJSON() + testutil.OK(t, err) + testutil.Equals(t, string(out), `{"staticPolicies":{"policy0":{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"}}},"templates":{"template0":{"effect":"permit","principal":{"op":"==","slot":"?principal"},"action":{"op":"All"},"resource":{"op":"All"}}}}`) + }) +} + +func TestAll(t *testing.T) { + t.Parallel() + t.Run("all", func(t *testing.T) { + t.Parallel() + + policies := map[cedar.PolicyID]*templates.Policy{ + "policy0": templates.NewPolicyFromAST(ast.Forbid()), + "policy1": templates.NewPolicyFromAST(ast.Forbid()), + "policy2": templates.NewPolicyFromAST(ast.Forbid()), + } + + ps := templates.NewPolicySet() + for k, v := range policies { + ps.Add(k, v) + } + + got := map[cedar.PolicyID]*templates.Policy{} + for k, v := range ps.All() { + got[k] = v + } + + testutil.Equals(t, policies, got) + }) + + t.Run("break early", func(t *testing.T) { + t.Parallel() + + policies := map[cedar.PolicyID]*templates.Policy{ + "policy0": templates.NewPolicyFromAST(ast.Forbid()), + "policy1": templates.NewPolicyFromAST(ast.Forbid()), + "policy2": templates.NewPolicyFromAST(ast.Forbid()), + } + + ps := templates.NewPolicySet() + for k, v := range policies { + ps.Add(k, v) + } + + got := map[cedar.PolicyID]*templates.Policy{} + for k, v := range ps.All() { + got[k] = v + if len(got) == 2 { + break + } + } + + testutil.Equals(t, len(got), 2) + for k, v := range got { + testutil.Equals(t, policies[k], v) + } + }) +} + func TestPolicySetTemplateManagement(t *testing.T) { t.Run("template round-trip", func(t *testing.T) { policySet := templates.NewPolicySet() diff --git a/x/exp/templates/policy_test.go b/x/exp/templates/policy_test.go new file mode 100644 index 00000000..8d80f473 --- /dev/null +++ b/x/exp/templates/policy_test.go @@ -0,0 +1,158 @@ +package templates_test + +import ( + "bytes" + "encoding/json" + "github.com/cedar-policy/cedar-go/x/exp/templates" + "testing" + + "github.com/cedar-policy/cedar-go" + "github.com/cedar-policy/cedar-go/ast" + "github.com/cedar-policy/cedar-go/internal/testutil" +) + +func prettifyJSON(in []byte) []byte { + var buf bytes.Buffer + _ = json.Indent(&buf, in, "", " ") + return buf.Bytes() +} + +func TestPolicyJSON(t *testing.T) { + t.Parallel() + + // Taken from https://docs.cedarpolicy.com/policies/json-format.html + jsonEncodedPolicy := prettifyJSON([]byte(` + { + "effect": "permit", + "principal": { + "op": "==", + "entity": { "type": "User", "id": "12UA45" } + }, + "action": { + "op": "==", + "entity": { "type": "Action", "id": "view" } + }, + "resource": { + "op": "in", + "entity": { "type": "Folder", "id": "abc" } + }, + "conditions": [ + { + "kind": "when", + "body": { + "==": { + "left": { + ".": { + "left": { + "Var": "context" + }, + "attr": "tls_version" + } + }, + "right": { + "Value": "1.3" + } + } + } + } + ] + }`, + )) + + var policy templates.Policy + testutil.OK(t, policy.UnmarshalJSON(jsonEncodedPolicy)) + + output, err := policy.MarshalJSON() + testutil.OK(t, err) + + testutil.Equals(t, string(prettifyJSON(output)), string(jsonEncodedPolicy)) +} + +func TestTemplateJSON(t *testing.T) { + t.Parallel() + + // Taken from https://docs.cedarpolicy.com/policies/json-format.html + jsonEncodedTemplate := prettifyJSON([]byte(` + { + "effect": "forbid", + "principal": { + "op": "==", + "entity": { "type": "User", "id": "12UA45" } + }, + "action": { + "op": "==", + "entity": { "type": "Action", "id": "view" } + }, + "resource": { + "op": "in", + "slot": "?resource" + }, + "conditions": [] + }`, + )) + + var policy templates.Template + testutil.OK(t, policy.UnmarshalJSON(jsonEncodedTemplate)) + + output, err := policy.MarshalJSON() + testutil.OK(t, err) + + testutil.Equals(t, string(prettifyJSON(output)), string(jsonEncodedTemplate)) +} + +func TestPolicyCedar(t *testing.T) { + t.Parallel() + + // Taken from https://docs.cedarpolicy.com/policies/syntax-policy.html + policyStr := `permit ( + principal, + action == Action::"editPhoto", + resource +) +when { resource.owner == principal };` + + var policy templates.Policy + testutil.OK(t, policy.UnmarshalCedar([]byte(policyStr))) + + testutil.Equals(t, string(policy.MarshalCedar()), policyStr) +} + +func TestTemplateCedar(t *testing.T) { + t.Parallel() + + policyStr := `permit ( + principal == ?principal, + action, + resource == ?resource +) +when { resource.owner == principal };` + + var policy templates.Template + testutil.OK(t, policy.UnmarshalCedar([]byte(policyStr))) + + testutil.Equals(t, string(policy.MarshalCedar()), policyStr) +} + +func TestPolicyAST(t *testing.T) { + t.Parallel() + + astExample := ast.Permit(). + ActionEq(cedar.NewEntityUID("Action", "editPhoto")). + When(ast.Resource().Access("owner").Equal(ast.Principal())) + + _ = templates.NewPolicyFromAST(astExample) +} + +func TestUnmarshalJSONPolicyErr(t *testing.T) { + t.Parallel() + var p templates.Policy + err := p.UnmarshalJSON([]byte("!@#$")) + testutil.Error(t, err) +} + +func TestUnmarshalCedarPolicyErr(t *testing.T) { + t.Parallel() + var p templates.Policy + err := p.UnmarshalCedar([]byte("!@#$")) + testutil.Error(t, err) +} diff --git a/x/exp/templates/template.go b/x/exp/templates/template.go deleted file mode 100644 index dac8432f..00000000 --- a/x/exp/templates/template.go +++ /dev/null @@ -1 +0,0 @@ -package templates From c014e5e389409c8fa5bfbc2c16ff24326e862632 Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Sat, 7 Jun 2025 11:54:13 -0300 Subject: [PATCH 21/24] feat: marshal/unmarshal template links Signed-off-by: Caio Ferreira --- internal/json/policy_set.go | 13 ++++++++++-- x/exp/templates/policy_set.go | 33 +++++++++++++++++++++++++++++- x/exp/templates/policy_set_test.go | 13 +++++++++--- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/internal/json/policy_set.go b/internal/json/policy_set.go index 6bc2793b..fb7d9122 100644 --- a/internal/json/policy_set.go +++ b/internal/json/policy_set.go @@ -1,10 +1,19 @@ package json +import "github.com/cedar-policy/cedar-go/types" + type PolicySet map[string]*Policy type TemplateSet map[string]*Policy +type LinkedPolicy struct { + TemplateID string `json:"templateId"` + LinkID string `json:"newId"` + Values map[string]types.ImplicitlyMarshaledEntityUID `json:"values"` +} + type PolicySetJSON struct { - StaticPolicies PolicySet `json:"staticPolicies"` - Templates TemplateSet `json:"templates"` + StaticPolicies PolicySet `json:"staticPolicies"` + Templates TemplateSet `json:"templates"` + TemplateLinks []LinkedPolicy `json:"templateLinks,omitempty"` } diff --git a/x/exp/templates/policy_set.go b/x/exp/templates/policy_set.go index d1000b84..7cee1ef2 100644 --- a/x/exp/templates/policy_set.go +++ b/x/exp/templates/policy_set.go @@ -122,7 +122,6 @@ func (p *PolicySet) MarshalCedar() []byte { return buf.Bytes() } -//todo: marshal links // MarshalJSON encodes a PolicySet in the JSON format specified by the [Cedar documentation]. // // [Cedar documentation]: https://docs.cedarpolicy.com/policies/json-format.html @@ -130,6 +129,7 @@ func (p *PolicySet) MarshalJSON() ([]byte, error) { jsonPolicySet := internaljson.PolicySetJSON{ StaticPolicies: make(internaljson.PolicySet, len(p.staticPolicies)), Templates: make(internaljson.TemplateSet, len(p.templates)), + TemplateLinks: make([]internaljson.LinkedPolicy, 0, len(p.linkedPolicies)), } for k, v := range p.staticPolicies { jsonPolicySet.StaticPolicies[string(k)] = (*internaljson.Policy)(v.AST()) @@ -137,6 +137,19 @@ func (p *PolicySet) MarshalJSON() ([]byte, error) { for k, v := range p.templates { jsonPolicySet.Templates[string(k)] = (*internaljson.Policy)(v.AST()) } + for _, v := range p.linkedPolicies { + lp := internaljson.LinkedPolicy{ + TemplateID: string(v.templateID), + LinkID: string(v.linkID), + Values: make(map[string]types.ImplicitlyMarshaledEntityUID, len(v.slotEnv)), + } + + for slotID, entityUID := range v.slotEnv { + lp.Values[string(slotID)] = types.ImplicitlyMarshaledEntityUID(entityUID) + } + + jsonPolicySet.TemplateLinks = append(jsonPolicySet.TemplateLinks, lp) + } return json.Marshal(jsonPolicySet) } @@ -152,6 +165,7 @@ func (p *PolicySet) UnmarshalJSON(b []byte) error { *p = PolicySet{ staticPolicies: make(PolicyMap, len(jsonPolicySet.StaticPolicies)), templates: make(map[cedar.PolicyID]*Template, len(jsonPolicySet.Templates)), + linkedPolicies: make(map[cedar.PolicyID]*LinkedPolicy), } for k, v := range jsonPolicySet.StaticPolicies { p.staticPolicies[cedar.PolicyID(k)] = newPolicy((*internalast.Policy)(v)) @@ -159,6 +173,23 @@ func (p *PolicySet) UnmarshalJSON(b []byte) error { for k, v := range jsonPolicySet.Templates { p.templates[cedar.PolicyID(k)] = newTemplate((*internalast.Policy)(v)) } + for _, v := range jsonPolicySet.TemplateLinks { + lp := &LinkedPolicy{ + templateID: cedar.PolicyID(v.TemplateID), + linkID: cedar.PolicyID(v.LinkID), + slotEnv: make(map[types.SlotID]types.EntityUID, len(v.Values)), + } + + for slotID, entityUID := range v.Values { + slotIDTyped := types.SlotID(slotID) + entityUIDTyped := types.EntityUID(entityUID) + + lp.slotEnv[slotIDTyped] = entityUIDTyped + } + + p.linkedPolicies[cedar.PolicyID(v.LinkID)] = lp + } + return nil } diff --git a/x/exp/templates/policy_set_test.go b/x/exp/templates/policy_set_test.go index c3632c11..c5e11e2e 100644 --- a/x/exp/templates/policy_set_test.go +++ b/x/exp/templates/policy_set_test.go @@ -161,10 +161,11 @@ func TestPolicySetJSON(t *testing.T) { t.Run("UnmarshalOK", func(t *testing.T) { t.Parallel() var ps templates.PolicySet - err := ps.UnmarshalJSON([]byte(`{"staticPolicies":{"policy0":{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"}}},"templates":{"template0":{"effect":"permit","principal":{"op":"==","slot":"?principal"},"action":{"op":"All"},"resource":{"op":"All"}}}}`)) + err := ps.UnmarshalJSON([]byte(`{"staticPolicies":{"policy0":{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"}}},"templates":{"template0":{"effect":"permit","principal":{"op":"==","slot":"?principal"},"action":{"op":"All"},"resource":{"op":"All"}}},"templateLinks":[{"templateId":"template0","newId":"linked0","values":{"?principal":{"type":"User","id":"alice"}}}]}`)) testutil.OK(t, err) - testutil.Equals(t, len(maps.Collect(ps.All())), 1) + testutil.Equals(t, len(maps.Collect(ps.All())), 2) testutil.Equals(t, ps.GetTemplate("template0") != nil, true) + testutil.Equals(t, ps.GetLinkedPolicy("linked0") != nil, true) }) t.Run("MarshalOK", func(t *testing.T) { @@ -173,9 +174,15 @@ func TestPolicySetJSON(t *testing.T) { permit (principal == ?principal, action, resource);`)) testutil.OK(t, err) + + err = ps.LinkTemplate("template0", "linked0", map[types.SlotID]types.EntityUID{ + "?principal": types.NewEntityUID("User", "alice"), + }) + testutil.OK(t, err) + out, err := ps.MarshalJSON() testutil.OK(t, err) - testutil.Equals(t, string(out), `{"staticPolicies":{"policy0":{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"}}},"templates":{"template0":{"effect":"permit","principal":{"op":"==","slot":"?principal"},"action":{"op":"All"},"resource":{"op":"All"}}}}`) + testutil.Equals(t, string(out), `{"staticPolicies":{"policy0":{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"},"conditions":[]}},"templates":{"template0":{"effect":"permit","principal":{"op":"==","slot":"?principal"},"action":{"op":"All"},"resource":{"op":"All"},"conditions":[]}},"templateLinks":[{"templateId":"template0","newId":"linked0","values":{"?principal":{"type":"User","id":"alice"}}}]}`) }) } From 501303e88f0f017d28ef517f20d70462941898da Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Sat, 7 Jun 2025 12:02:24 -0300 Subject: [PATCH 22/24] test: add policy slice test in templates Signed-off-by: Caio Ferreira --- x/exp/templates/policy_list_test.go | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 x/exp/templates/policy_list_test.go diff --git a/x/exp/templates/policy_list_test.go b/x/exp/templates/policy_list_test.go new file mode 100644 index 00000000..d7f8944a --- /dev/null +++ b/x/exp/templates/policy_list_test.go @@ -0,0 +1,56 @@ +package templates_test + +import ( + "github.com/cedar-policy/cedar-go/x/exp/templates" + "testing" + + "github.com/cedar-policy/cedar-go/internal/testutil" +) + +func TestPolicySlice(t *testing.T) { + t.Parallel() + + policiesStr := `permit ( + principal, + action == Action::"editPhoto", + resource +) +when { resource.owner == principal }; + +forbid ( + principal in Groups::"bannedUsers", + action, + resource +);` + + policies, err := templates.NewPolicyListFromBytes("", []byte(policiesStr)) + testutil.OK(t, err) + testutil.Equals(t, string(policies.MarshalCedar()), policiesStr) +} + +func TestPolicyWithTemplateSlice(t *testing.T) { + t.Parallel() + + policiesStr := `permit ( + principal, + action == Action::"editPhoto", + resource +) +when { resource.owner == principal }; + +forbid ( + principal in Groups::"bannedUsers", + action, + resource +); + +permit ( + principal == ?principal, + action, + resource +);` + + policies, err := templates.NewPolicyListFromBytes("", []byte(policiesStr)) + testutil.OK(t, err) + testutil.Equals(t, string(policies.MarshalCedar()), policiesStr) +} From 8f09e06d4b16b40e41cd9f244fbc84162debd1b3 Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Sat, 7 Jun 2025 12:07:08 -0300 Subject: [PATCH 23/24] chore: add godocs Signed-off-by: Caio Ferreira --- x/exp/templates/policy.go | 15 +++++++++------ x/exp/templates/policy_list.go | 15 ++++++++------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/x/exp/templates/policy.go b/x/exp/templates/policy.go index 06a1a2e9..10f5d050 100644 --- a/x/exp/templates/policy.go +++ b/x/exp/templates/policy.go @@ -2,6 +2,7 @@ package templates import ( "bytes" + "github.com/cedar-policy/cedar-go" "github.com/cedar-policy/cedar-go/types" @@ -112,6 +113,7 @@ func (p *Policy) AST() *ast.Policy { // to create concrete policies. It's a wrapper around the internal parser.Policy type. type Template parser.Policy +// newTemplate creates a new Template from the given internal AST Policy. func newTemplate(astIn *internalast.Policy) *Template { t := (*Template)(astIn) return t @@ -128,6 +130,8 @@ func (p *Template) MarshalCedar() []byte { return buf.Bytes() } +// UnmarshalCedar parses and compiles a single Template statement in the human-readable format specified by the Cedar documentation. +// Returns an error if parsing fails. func (p *Template) UnmarshalCedar(b []byte) error { var cedarPolicy parser.Policy if err := cedarPolicy.UnmarshalCedar(b); err != nil { @@ -139,9 +143,8 @@ func (p *Template) UnmarshalCedar(b []byte) error { return nil } -// 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 +// MarshalJSON encodes a single Template statement in the JSON format specified by the Cedar documentation. +// Returns the JSON-encoded template as a byte slice, or an error if encoding fails. func (p *Template) MarshalJSON() ([]byte, error) { policyAST := (*internalast.Policy)(p) jsonPolicy := (*json.Policy)(policyAST) @@ -149,9 +152,8 @@ func (p *Template) MarshalJSON() ([]byte, error) { 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 +// UnmarshalJSON parses and compiles a single Template statement in the JSON format specified by the Cedar documentation. +// Returns an error if parsing fails. func (p *Template) UnmarshalJSON(b []byte) error { var jsonPolicy json.Policy if err := jsonPolicy.UnmarshalJSON(b); err != nil { @@ -169,6 +171,7 @@ func (p *Template) SetFilename(fileName string) { p.Position.Filename = fileName } +// Slots returns the slot IDs used in this template. func (p *Template) Slots() []types.SlotID { policyAST := (*internalast.Policy)(p) return policyAST.Slots() diff --git a/x/exp/templates/policy_list.go b/x/exp/templates/policy_list.go index a11bb910..94fc610f 100644 --- a/x/exp/templates/policy_list.go +++ b/x/exp/templates/policy_list.go @@ -3,6 +3,7 @@ package templates import ( "bytes" "fmt" + "github.com/cedar-policy/cedar-go/ast" "github.com/cedar-policy/cedar-go/internal/parser" ) @@ -10,12 +11,12 @@ import ( // PolicyList represents a list of un-named Policy's. Cedar documents, unlike the PolicySet form, don't have a means of // naming individual policies. type PolicyList struct { - StaticPolicies []*Policy - Templates []*Template + StaticPolicies []*Policy // StaticPolicies holds the list of static (non-template) policies. + Templates []*Template // Templates holds the list of policy templates. } -// NewPolicyListFromBytes will create a Policies from the given text document with the given file name used in Position -// data. If there is an error parsing the document, it will be returned. +// NewPolicyListFromBytes creates a PolicyList from the given Cedar policy document bytes and assigns the provided file name +// to each policy and template for position tracking. Returns an error if parsing fails. func NewPolicyListFromBytes(fileName string, document []byte) (PolicyList, error) { var policySlice PolicyList if err := policySlice.UnmarshalCedar(document); err != nil { @@ -32,8 +33,8 @@ func NewPolicyListFromBytes(fileName string, document []byte) (PolicyList, error return policySlice, nil } -// UnmarshalCedar parses a concatenation of un-named Cedar policy statements. Names can be assigned to these policies -// when adding them to a PolicySet. +// UnmarshalCedar parses a concatenation of un-named Cedar policy statements from the provided byte slice and populates +// the PolicyList with static policies and templates. Returns an error if parsing fails. func (p *PolicyList) UnmarshalCedar(b []byte) error { var res parser.PolicySlice if err := res.UnmarshalCedar(b); err != nil { @@ -58,7 +59,7 @@ func (p *PolicyList) UnmarshalCedar(b []byte) error { return nil } -// MarshalCedar emits a concatenated Cedar representation of the policies. +// MarshalCedar emits a concatenated Cedar representation of the policies and templates in the PolicyList as a byte slice. func (p PolicyList) MarshalCedar() []byte { var buf bytes.Buffer for i, policy := range p.StaticPolicies { From 9d4eeff60c5cb717e8aab91ee124266fc176af8c Mon Sep 17 00:00:00 2001 From: Caio Ferreira Date: Sat, 7 Jun 2025 12:14:16 -0300 Subject: [PATCH 24/24] chore: add experimental templates readme Signed-off-by: Caio Ferreira --- x/exp/templates/README.md | 196 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 x/exp/templates/README.md diff --git a/x/exp/templates/README.md b/x/exp/templates/README.md new file mode 100644 index 00000000..28c6e074 --- /dev/null +++ b/x/exp/templates/README.md @@ -0,0 +1,196 @@ +# Cedar Templates for Go + +Cedar Templates is a feature that extends the Cedar policy language in Go by allowing you to create policy templates with placeholder variables that can be filled in at runtime. This README explains the basics of Cedar Templates and provides examples of how to use them. + +## Overview + +Cedar policy language provides a way to define access control policies for your applications. Templates enhance this capability by allowing you to create policy patterns that can be instantiated with specific values at runtime. This is particularly useful when you need to create similar policies for different entities without duplicating policy code. + +## Key Concepts + +- **Template**: A Cedar policy with placeholders (slots) that can be filled in at runtime. +- **Slots**: Placeholders in a template denoted by a question mark followed by an identifier (e.g., `?principal`). +- **Linking**: The process of binding concrete values to slots in a template to create a usable policy. +- **PolicySet**: A collection of policies and templates that can be used for authorization decisions. + +## Basic Usage + +### Creating a Template + +A template looks like a regular Cedar policy but includes slots (marked with `?`) for values to be filled in later: + +```go +templateStr := `permit ( + principal == ?principal, + action, + resource == ?resource +) +when { resource.owner == principal };` + +var template templates.Template +err := template.UnmarshalCedar([]byte(templateStr)) +if err != nil { + // handle error +} +``` + +### Creating a PolicySet and Adding Templates + +```go +// Create a new empty PolicySet +policySet := templates.NewPolicySet() + +// Add a template to the PolicySet +templateID := cedar.PolicyID("access_template") +policySet.AddTemplate(templateID, &template) +``` + +### Linking a Template to Create a Policy + +Once you have a template, you can link it with specific entity values to create a concrete policy: + +```go +// Define the slot values +slotValues := map[types.SlotID]types.EntityUID{ + "?principal": types.NewEntityUID("User", "alice"), + "?resource": types.NewEntityUID("Document", "report"), +} + +// Link the template to create a policy +linkID := cedar.PolicyID("alice_report_access") +err = policySet.LinkTemplate(templateID, linkID, slotValues) +if err != nil { + // handle error +} +``` + +### Using Templates for Authorization + +```go +// Create a request +request := cedar.Request{ + Principal: cedar.NewEntityUID("User", "alice"), + Action: cedar.NewEntityUID("Action", "read"), + Resource: cedar.NewEntityUID("Document", "report"), + Context: types.NewRecord(nil), +} + +// Create an entity store with relevant entities +entities := types.NewEntityMap() +// Add entities to the store... + +// Make an authorization decision +decision, diagnostic := templates.Authorize(policySet, entities, request) + +// Check the decision +if decision == cedar.Allow { + // Access granted +} else { + // Access denied +} +``` + +## Advanced Examples + +### Example 1: Role-Based Access Control Template + +```go +// Template that grants access based on role +roleBasedTemplate := `permit ( + principal, + action, + resource +) +when { principal.roles.contains(?role) };` + +// Link with a specific role +roleSlots := map[types.SlotID]types.EntityUID{ + "?role": types.NewEntityUID("Role", "admin"), +} +policySet.LinkTemplate(cedar.PolicyID("role_template"), cedar.PolicyID("admin_access"), roleSlots) +``` + +### Example 2: Resource Ownership Template + +```go +// Template for resource ownership +ownershipTemplate := `permit ( + principal == ?owner, + action in [Action::"read", Action::"write", Action::"delete"], + resource == ?resource +);` + +// Link with specific owner and resource +ownershipSlots := map[types.SlotID]types.EntityUID{ + "?owner": types.NewEntityUID("User", "bob"), + "?resource": types.NewEntityUID("Photo", "vacation"), +} +policySet.LinkTemplate(cedar.PolicyID("ownership_template"), cedar.PolicyID("bob_photo_ownership"), ownershipSlots) +``` + +### Example 3: Handling Multiple Templates + +```go +// Load templates from Cedar language text +policySetStr := ` +// Resource ownership template +template ownership_tpl(principal, resource) { + permit( + principal == ?principal, + action in [Action::"read", Action::"write"], + resource == ?resource + ); +} + +// Role-based access template +template role_tpl(role) { + permit( + principal, + action, + resource + ) + when { principal.roles.contains(?role) }; +} +` + +policySet, err := templates.NewPolicySetFromBytes("policies.cedar", []byte(policySetStr)) +if err != nil { + // handle error +} + +// Link templates +policySet.LinkTemplate("ownership_tpl", "alice_doc1_ownership", map[types.SlotID]types.EntityUID{ + "?principal": types.NewEntityUID("User", "alice"), + "?resource": types.NewEntityUID("Document", "doc1"), +}) + +policySet.LinkTemplate("role_tpl", "admin_access", map[types.SlotID]types.EntityUID{ + "?role": types.NewEntityUID("Role", "admin"), +}) +``` + +## Working with Template Outputs + +After linking a template, the resulting policy can be: + +1. Used for authorization via the `templates.Authorize()` function +2. Serialized to Cedar language format with `MarshalCedar()` +3. Serialized to JSON format with `MarshalJSON()` + +## Notes and Best Practices + +1. **Template Management**: Keep track of template IDs and linked policy IDs to manage them effectively. +2. **Error Handling**: Always check for errors when parsing templates, linking them, or making authorization decisions. +3. **Entity Management**: Ensure your entity store contains all entities referenced in your policies and templates. +4. **Slot Validation**: Verify that all required slots are provided when linking a template. +5. **Experimental Status**: Note that the templates package is in the experimental (`x/exp`) namespace and may undergo changes. + +## Additional Resources + +- [Cedar Policy Documentation](https://docs.cedarpolicy.com/) +- [Cedar Templates Documentation](https://docs.cedarpolicy.com/policies/templates.html) +- [Go API Reference](https://pkg.go.dev/github.com/cedar-policy/cedar-go) + +## License + +Cedar is licensed under the Apache License, Version 2.0 \ No newline at end of file