From a0a56a9a5e0d63b076b10d1545ff10d048cbbab8 Mon Sep 17 00:00:00 2001 From: Kevin Jamieson Date: Fri, 29 Nov 2024 08:54:34 -0800 Subject: [PATCH 1/4] types: add a Get method to EntityMap This is just a wrapper around the underlying map get, in preparation for introducing an EntityGetter interface. Signed-off-by: Kevin Jamieson --- types/entity_map.go | 5 +++++ types/entity_map_test.go | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/types/entity_map.go b/types/entity_map.go index 70b9034..b478eab 100644 --- a/types/entity_map.go +++ b/types/entity_map.go @@ -37,3 +37,8 @@ func (e *EntityMap) UnmarshalJSON(b []byte) error { func (e EntityMap) Clone() EntityMap { return maps.Clone(e) } + +func (e EntityMap) Get(uid EntityUID) (Entity, bool) { + ent, ok := e[uid] + return ent, ok +} diff --git a/types/entity_map_test.go b/types/entity_map_test.go index 507e38d..a8a2afe 100644 --- a/types/entity_map_test.go +++ b/types/entity_map_test.go @@ -22,6 +22,21 @@ func TestEntities(t *testing.T) { testutil.Equals(t, clone, e) }) + t.Run("Get", func(t *testing.T) { + t.Parallel() + ent := types.Entity{ + UID: types.NewEntityUID("Type", "id"), + Attributes: types.NewRecord(types.RecordMap{"key": types.Long(42)}), + } + e := types.EntityMap{ + ent.UID: ent, + } + got, ok := e.Get(ent.UID) + testutil.Equals(t, ok, true) + testutil.Equals(t, got, ent) + _, ok = e.Get(types.NewEntityUID("Type", "id2")) + testutil.Equals(t, ok, false) + }) } func TestEntitiesJSON(t *testing.T) { From 974066347bfa79cbd44aa4bd92613c5567b5ddbb Mon Sep 17 00:00:00 2001 From: Kevin Jamieson Date: Fri, 29 Nov 2024 08:55:53 -0800 Subject: [PATCH 2/4] types: define EntityGetter interface This defines an interface for retrieving an Entity by EntityUID, implemented by EntityMap, in preparation for using this in IsAuthorized(). Signed-off-by: Kevin Jamieson --- types/entity_map.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/types/entity_map.go b/types/entity_map.go index b478eab..7de8039 100644 --- a/types/entity_map.go +++ b/types/entity_map.go @@ -8,6 +8,13 @@ import ( "golang.org/x/exp/maps" ) +// An EntityGetter is an interface for retrieving an Entity by EntityUID. +type EntityGetter interface { + Get(uid EntityUID) (Entity, bool) +} + +var _ EntityGetter = EntityMap{} + // An EntityMap is a collection of all the entities that are needed to evaluate // authorization requests. The key is an EntityUID which uniquely identifies // the Entity (it must be the same as the UID within the Entity itself.) From de8982290c7ac36b18af13f757c6827317f6acd7 Mon Sep 17 00:00:00 2001 From: Kevin Jamieson Date: Fri, 29 Nov 2024 08:56:25 -0800 Subject: [PATCH 3/4] cedar-go: use EntityGetter interface in PolicySet.IsAuthorized() This replaces the concrete EntityMap passed to IsAuthorized() with an EntityGetter interface, allowing alternative implementations of this interface to be supplied for authorization. While this is hypothetically a breaking change, we think that is extremely unlikely to be the case in practice. Any code passing an EntityMap will continue to work - the only potential breakage would be if a caller were directly passing a map[EntityUID]Entity here. Signed-off-by: Kevin Jamieson --- authorize.go | 2 +- internal/eval/evalers.go | 14 +++++++------- internal/eval/partial.go | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/authorize.go b/authorize.go index 302d6d7..d8164fa 100644 --- a/authorize.go +++ b/authorize.go @@ -18,7 +18,7 @@ const ( // IsAuthorized uses the combination of the PolicySet and Entities to determine // if the given Request to determine Decision and Diagnostic. -func (p PolicySet) IsAuthorized(entities types.EntityMap, req Request) (Decision, Diagnostic) { +func (p PolicySet) IsAuthorized(entities types.EntityGetter, req Request) (Decision, Diagnostic) { env := eval.Env{ Entities: entities, Principal: req.Principal, diff --git a/internal/eval/evalers.go b/internal/eval/evalers.go index 4ca1bce..d8cded9 100644 --- a/internal/eval/evalers.go +++ b/internal/eval/evalers.go @@ -22,7 +22,7 @@ func zeroValue() types.Value { } type Env struct { - Entities types.EntityMap + Entities types.EntityGetter Principal, Action, Resource types.Value Context types.Value } @@ -754,7 +754,7 @@ func (n *attributeAccessEval) Eval(env Env) (types.Value, error) { if vv == unspecified { return zeroValue(), fmt.Errorf("cannot access attribute `%s` of %w", n.attribute, errUnspecifiedEntity) } - rec, ok := env.Entities[vv] + rec, ok := env.Entities.Get(vv) if !ok { return zeroValue(), fmt.Errorf("entity `%v` %w", vv.String(), errEntityNotExist) } @@ -792,7 +792,7 @@ func (n *hasEval) Eval(env Env) (types.Value, error) { var record types.Record switch vv := v.(type) { case types.EntityUID: - if rec, ok := env.Entities[vv]; ok { + if rec, ok := env.Entities.Get(vv); ok { record = rec.Attributes } case types.Record: @@ -861,12 +861,12 @@ func entityInOne(env Env, entity types.EntityUID, parent types.EntityUID) bool { var todo []types.EntityUID var candidate = entity for { - if fe, ok := env.Entities[candidate]; ok { + if fe, ok := env.Entities.Get(candidate); ok { if fe.Parents.Contains(parent) { return true } fe.Parents.Iterate(func(k types.EntityUID) bool { - p, ok := env.Entities[k] + p, ok := env.Entities.Get(k) if !ok || p.Parents.Len() == 0 || k == entity || known.Contains(k) { return true } @@ -890,12 +890,12 @@ func entityInSet(env Env, entity types.EntityUID, parents mapset.Container[types var todo []types.EntityUID var candidate = entity for { - if fe, ok := env.Entities[candidate]; ok { + if fe, ok := env.Entities.Get(candidate); ok { if fe.Parents.Intersects(parents) { return true } fe.Parents.Iterate(func(k types.EntityUID) bool { - p, ok := env.Entities[k] + p, ok := env.Entities.Get(k) if !ok || p.Parents.Len() == 0 || k == entity || known.Contains(k) { return true } diff --git a/internal/eval/partial.go b/internal/eval/partial.go index 07106cd..33dede4 100644 --- a/internal/eval/partial.go +++ b/internal/eval/partial.go @@ -504,7 +504,7 @@ func (n *partialHasEval) Eval(env Env) (types.Value, error) { var record types.Record switch vv := v.(type) { case types.EntityUID: - if rec, ok := env.Entities[vv]; ok { + if rec, ok := env.Entities.Get(vv); ok { record = rec.Attributes } case types.Record: From 316f277672632a64653418c6cc736d8062a24b11 Mon Sep 17 00:00:00 2001 From: Kevin Jamieson Date: Mon, 2 Dec 2024 13:22:32 -0800 Subject: [PATCH 4/4] cedar-go: clarify current policy around backwards compatibility At present time, the maintainers are reserving the right to: * Add variadics to existing functions/methods. * Replace concrete types with interfaces in the parameters to functions/methods (as occurred in this patch set). While strictly breaking changes, we are asserting that the benefits of being able to make changes of these forms without a major revision outweigh the unlikely possibility of breaking code written against cedar-go. We may of course choose to revisit and revise these policies in the future, especially as cedar-go gains wider usage. Signed-off-by: Kevin Jamieson --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 711da17..112f5f4 100644 --- a/README.md +++ b/README.md @@ -137,8 +137,9 @@ Generated documentation for the latest version of the Go implementation can be a If you're looking to integrate Cedar into a production system, please be sure the read the [security best practices](https://docs.cedarpolicy.com/other/security.html) ## Backward Compatibility Considerations - -x/exp - code in this directory is not subject to the semantic version constraints of the rest of the module and breaking changes may be made at any time +- `x/exp` - code in this directory is not subject to the semantic versioning constraints of the rest of the module and breaking changes may be made at any time. +- Variadics may be added to functions that do not have them to expand the arguments of a function or method. +- Concrete types may be replaced with compatible interfaces to expand the variety of arguments a function or method can take. ## Change log