Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e91a8b0
feat: implement policy templates
caiorcferreira Apr 2, 2025
a5b5f31
feat: add SetFilename method to Template struct
caiorcferreira Apr 3, 2025
00f2562
feat: set filename in template
caiorcferreira Apr 3, 2025
1116e25
feat: add template management methods to PolicySet
caiorcferreira Apr 3, 2025
b8c96c8
chore: add godoc to main public types and functions
caiorcferreira Apr 3, 2025
b897f96
test: get, add and remove template functions
caiorcferreira Apr 3, 2025
276efe5
feat: fail if both entity and slot are set in json format
caiorcferreira Apr 3, 2025
071be6d
Merge branch 'main' of github.com:caiorcferreira/cedar-go into feat/t…
caiorcferreira May 14, 2025
cc0491d
feat: revert public types and functions to work over only static poli…
caiorcferreira May 14, 2025
db4a0be
feat: move template code to experimental package
caiorcferreira May 14, 2025
00dc21b
refact: use public types to avoid duplicating Authorize function
caiorcferreira May 14, 2025
195cb70
feat: return error on link template when conditions are invalid
caiorcferreira May 14, 2025
608d6c5
refact: remove variable slot type
caiorcferreira Jun 7, 2025
4800346
refact: make add slot private and use builder methods to manage slots
caiorcferreira Jun 7, 2025
536bddc
refact: remove Slot method from interface and validate when parsing it
caiorcferreira Jun 7, 2025
b178952
refact: replicate policy to templates package
caiorcferreira Jun 7, 2025
97dee92
tests: fix templates tests
caiorcferreira Jun 7, 2025
6eccb26
feat: remove linked policies once template is removed
caiorcferreira Jun 7, 2025
39bdf0e
feat: add support to remove links and ensure that link ids are unique
caiorcferreira Jun 7, 2025
eb2f7d2
fix: original tests
caiorcferreira Jun 7, 2025
ae8069f
feat: add all marshal/unmarshal methods to template
caiorcferreira Jun 7, 2025
c014e5e
feat: marshal/unmarshal template links
caiorcferreira Jun 7, 2025
501303e
test: add policy slice test in templates
caiorcferreira Jun 7, 2025
8f09e06
chore: add godocs
caiorcferreira Jun 7, 2025
9d4eeff
chore: add experimental templates readme
caiorcferreira Jun 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion authorize.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()})
Expand Down
17 changes: 14 additions & 3 deletions internal/eval/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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))
}
}
4 changes: 2 additions & 2 deletions internal/eval/partial.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
4 changes: 3 additions & 1 deletion internal/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
36 changes: 30 additions & 6 deletions internal/json/json_marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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))
Expand Down
48 changes: 48 additions & 0 deletions internal/json/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,54 @@ 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,
},
{
"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 {
Expand Down
122 changes: 112 additions & 10 deletions internal/json/json_unmarshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,109 @@ 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("both entity and slot are set")
}

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("both entity and slot are set")
}

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) {
switch s.Op {
case "All":
return ast.Scope{}.All(), nil
case "==":
if s.Entity == nil {
return nil, fmt.Errorf("missing entity")
ref, err := scopeEntityReference(s)
if err != nil {
return nil, err
}
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)
}

Expand Down Expand Up @@ -304,19 +385,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 {
Expand Down
5 changes: 4 additions & 1 deletion internal/json/policy_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Loading