Skip to content

Commit d9c6d61

Browse files
authored
Merge pull request #77 from strongdm/tag-support
RFC-82: Implement support for Entity Tags
2 parents 96529dc + 21b073c commit d9c6d61

32 files changed

+679
-11
lines changed

README.md

-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ The Go implementation does not yet include:
4444
- schema support and the [validator](https://docs.cedarpolicy.com/policies/validation.html)
4545
- the formatter
4646
- partial evaluation
47-
- support for [RFC 82](https://github.com/cedar-policy/rfcs/blob/main/text/0082-entity-tags.md) (entity tags)
4847
- support for [policy templates](https://docs.cedarpolicy.com/policies/templates.html)
4948

5049
## Quick Start

ast/ast_test.go

+10
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,16 @@ func TestASTByTable(t *testing.T) {
347347
ast.Permit().When(ast.Long(42).Has("key")),
348348
internalast.Permit().When(internalast.Long(42).Has("key")),
349349
},
350+
{
351+
"opGetTag",
352+
ast.Permit().When(ast.EntityUID("T", "1").GetTag(ast.String("key"))),
353+
internalast.Permit().When(internalast.EntityUID("T", "1").GetTag(internalast.String("key"))),
354+
},
355+
{
356+
"opsHasTag",
357+
ast.Permit().When(ast.EntityUID("T", "1").HasTag(ast.String("key"))),
358+
internalast.Permit().When(internalast.EntityUID("T", "1").HasTag(internalast.String("key"))),
359+
},
350360
{
351361
"opIsIpv4",
352362
ast.Permit().When(ast.Long(42).IsIpv4()),

ast/operator.go

+8
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,14 @@ func (lhs Node) Has(attr types.String) Node {
140140
return wrapNode(lhs.Node.Has(attr))
141141
}
142142

143+
func (lhs Node) GetTag(rhs Node) Node {
144+
return wrapNode(lhs.Node.GetTag(rhs.Node))
145+
}
146+
147+
func (lhs Node) HasTag(rhs Node) Node {
148+
return wrapNode(lhs.Node.HasTag(rhs.Node))
149+
}
150+
143151
// ___ ____ _ _ _
144152
// |_ _| _ \ / \ __| | __| |_ __ ___ ___ ___
145153
// | || |_) / _ \ / _` |/ _` | '__/ _ \/ __/ __|

authorize.go

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ const (
1919
// IsAuthorized uses the combination of the PolicySet and Entities to determine
2020
// if the given Request to determine Decision and Diagnostic.
2121
func (p PolicySet) IsAuthorized(entities types.EntityGetter, req Request) (Decision, Diagnostic) {
22+
if entities == nil {
23+
var zero types.EntityMap
24+
entities = zero
25+
}
2226
env := eval.Env{
2327
Entities: entities,
2428
Principal: req.Principal,

authorize_test.go

+30-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/cedar-policy/cedar-go"
77
"github.com/cedar-policy/cedar-go/internal/testutil"
8+
"github.com/cedar-policy/cedar-go/types"
89
)
910

1011
//nolint:revive // due to table test function-length
@@ -15,7 +16,7 @@ func TestIsAuthorized(t *testing.T) {
1516
tests := []struct {
1617
Name string
1718
Policy string
18-
Entities cedar.EntityMap
19+
Entities types.EntityGetter
1920
Principal, Action, Resource cedar.EntityUID
2021
Context cedar.Record
2122
Want cedar.Decision
@@ -33,6 +34,34 @@ func TestIsAuthorized(t *testing.T) {
3334
Want: true,
3435
DiagErr: 0,
3536
},
37+
{
38+
Name: "permit-when-tags",
39+
Policy: `permit(principal,action,resource) when { principal.hasTag("foo") };`,
40+
Entities: types.EntityMap{
41+
cuzco: types.Entity{
42+
Tags: types.NewRecord(cedar.RecordMap{
43+
"foo": types.String("bar"),
44+
}),
45+
},
46+
},
47+
Principal: cuzco,
48+
Action: dropTable,
49+
Resource: cedar.NewEntityUID("table", "whatever"),
50+
Context: cedar.Record{},
51+
Want: true,
52+
DiagErr: 0,
53+
},
54+
{
55+
Name: "nil-entity-getter",
56+
Policy: `permit(principal,action,resource);`,
57+
Entities: nil,
58+
Principal: cuzco,
59+
Action: dropTable,
60+
Resource: cedar.NewEntityUID("table", "whatever"),
61+
Context: cedar.Record{},
62+
Want: true,
63+
DiagErr: 0,
64+
},
3665
{
3766
Name: "simple-forbid",
3867
Policy: `forbid(principal,action,resource);`,

corpus-tests.tar.gz

-744 KB
Binary file not shown.

corpus_test.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/cedar-policy/cedar-go"
1616
"github.com/cedar-policy/cedar-go/internal/testutil"
17+
"github.com/cedar-policy/cedar-go/types"
1718
"github.com/cedar-policy/cedar-go/x/exp/batch"
1819
)
1920

@@ -244,6 +245,7 @@ func TestCorpusRelated(t *testing.T) {
244245
tests := []struct {
245246
name string
246247
policy string
248+
entities types.EntityGetter
247249
request cedar.Request
248250
decision cedar.Decision
249251
reasons []cedar.PolicyID
@@ -258,6 +260,7 @@ func TestCorpusRelated(t *testing.T) {
258260
) when {
259261
(true && (((!870985681610) == principal) == principal)) && principal
260262
};`,
263+
nil,
261264
cedar.Request{Principal: cedar.NewEntityUID("a", "\u0000\u0000"), Action: cedar.NewEntityUID("Action", "action"), Resource: cedar.NewEntityUID("a", "\u0000\u0000")},
262265
cedar.Deny,
263266
nil,
@@ -273,6 +276,7 @@ func TestCorpusRelated(t *testing.T) {
273276
) when {
274277
(((!870985681610) == principal) == principal)
275278
};`,
279+
nil,
276280
cedar.Request{Principal: cedar.NewEntityUID("a", "\u0000\u0000"), Action: cedar.NewEntityUID("Action", "action"), Resource: cedar.NewEntityUID("a", "\u0000\u0000")},
277281
cedar.Deny,
278282
nil,
@@ -287,6 +291,7 @@ func TestCorpusRelated(t *testing.T) {
287291
) when {
288292
((!870985681610) == principal)
289293
};`,
294+
nil,
290295
cedar.Request{Principal: cedar.NewEntityUID("a", "\u0000\u0000"), Action: cedar.NewEntityUID("Action", "action"), Resource: cedar.NewEntityUID("a", "\u0000\u0000")},
291296
cedar.Deny,
292297
nil,
@@ -302,6 +307,7 @@ func TestCorpusRelated(t *testing.T) {
302307
) when {
303308
(!870985681610)
304309
};`,
310+
nil,
305311
cedar.Request{Principal: cedar.NewEntityUID("a", "\u0000\u0000"), Action: cedar.NewEntityUID("Action", "action"), Resource: cedar.NewEntityUID("a", "\u0000\u0000")},
306312
cedar.Deny,
307313
nil,
@@ -317,6 +323,7 @@ func TestCorpusRelated(t *testing.T) {
317323
) when {
318324
((!42) == principal)
319325
};`,
326+
nil,
320327
cedar.Request{},
321328
cedar.Deny,
322329
nil,
@@ -332,6 +339,7 @@ func TestCorpusRelated(t *testing.T) {
332339
) when {
333340
(!42 == principal)
334341
};`,
342+
nil,
335343
cedar.Request{},
336344
cedar.Deny,
337345
nil,
@@ -346,6 +354,7 @@ func TestCorpusRelated(t *testing.T) {
346354
) when {
347355
true && ((if (principal in action) then (ip("")) else (if true then (ip("6b6b:f00::32ff:ffff:6368/00")) else (ip("7265:6c69:706d:6f43:5f74:6f70:7374:6f68")))).isMulticast())
348356
};`,
357+
nil,
349358
cedar.Request{Principal: cedar.NewEntityUID("a", "\u0000\b\u0011\u0000R"), Action: cedar.NewEntityUID("Action", "action"), Resource: cedar.NewEntityUID("a", "\u0000\b\u0011\u0000R")},
350359
cedar.Deny,
351360
nil,
@@ -360,6 +369,7 @@ func TestCorpusRelated(t *testing.T) {
360369
) when {
361370
true && ip("6b6b:f00::32ff:ffff:6368/00").isMulticast()
362371
};`,
372+
nil,
363373
cedar.Request{},
364374
cedar.Deny,
365375
nil,
@@ -386,7 +396,7 @@ func TestCorpusRelated(t *testing.T) {
386396
t.Parallel()
387397
policy, err := cedar.NewPolicySetFromBytes("", []byte(tt.policy))
388398
testutil.OK(t, err)
389-
ok, diag := policy.IsAuthorized(cedar.EntityMap{}, tt.request)
399+
ok, diag := policy.IsAuthorized(tt.entities, tt.request)
390400
testutil.Equals(t, ok, tt.decision)
391401
var reasons []cedar.PolicyID
392402
for _, n := range diag.Reasons {

internal/eval/convert.go

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ func toEval(n ast.IsNode) Evaler {
1414
return newAttributeAccessEval(toEval(v.Arg), v.Value)
1515
case ast.NodeTypeHas:
1616
return newHasEval(toEval(v.Arg), v.Value)
17+
case ast.NodeTypeGetTag:
18+
return newGetTagEval(toEval(v.Left), toEval(v.Right))
19+
case ast.NodeTypeHasTag:
20+
return newHasTagEval(toEval(v.Left), toEval(v.Right))
1721
case ast.NodeTypeLike:
1822
return newLikeEval(toEval(v.Arg), v.Value)
1923
case ast.NodeTypeIfThenElse:

internal/eval/convert_test.go

+17-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ func TestToEval(t *testing.T) {
3030
types.True,
3131
testutil.OK,
3232
},
33+
{
34+
"getTag",
35+
ast.EntityUID("T", "ID").GetTag(ast.String("key")),
36+
types.Long(42),
37+
testutil.OK,
38+
},
39+
{
40+
"hasTag",
41+
ast.EntityUID("T", "ID").HasTag(ast.String("key")),
42+
types.True,
43+
testutil.OK,
44+
},
3345
{
3446
"like",
3547
ast.String("test").Like(types.Pattern{}),
@@ -355,7 +367,11 @@ func TestToEval(t *testing.T) {
355367
Action: types.NewEntityUID("Action", "test"),
356368
Resource: types.NewEntityUID("Resource", "database"),
357369
Context: types.Record{},
358-
Entities: types.EntityMap{},
370+
Entities: types.EntityMap{
371+
types.NewEntityUID("T", "ID"): types.Entity{
372+
Tags: types.NewRecord(types.RecordMap{"key": types.Long(42)}),
373+
},
374+
},
359375
})
360376
tt.err(t, err)
361377
testutil.Equals(t, out, tt.out)

internal/eval/evalers.go

+69-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ var errOverflow = fmt.Errorf("integer overflow")
1414
var errUnknownExtensionFunction = fmt.Errorf("function does not exist")
1515
var errArity = fmt.Errorf("wrong number of arguments provided to extension function")
1616
var errAttributeAccess = fmt.Errorf("does not have the attribute")
17+
var errTagAccess = fmt.Errorf("does not have the tag")
1718
var errEntityNotExist = fmt.Errorf("does not exist")
1819
var errUnspecifiedEntity = fmt.Errorf("unspecified entity")
1920

@@ -804,6 +805,73 @@ func (n *hasEval) Eval(env Env) (types.Value, error) {
804805
return types.Boolean(ok), nil
805806
}
806807

808+
// getTagEval
809+
type getTagEval struct {
810+
lhs, rhs Evaler
811+
}
812+
813+
func newGetTagEval(object, tag Evaler) *getTagEval {
814+
return &getTagEval{lhs: object, rhs: tag}
815+
}
816+
817+
func (n *getTagEval) Eval(env Env) (types.Value, error) {
818+
eid, err := evalEntity(n.lhs, env)
819+
if err != nil {
820+
return zeroValue(), err
821+
}
822+
823+
var zero types.EntityUID
824+
if eid == zero {
825+
return zeroValue(), fmt.Errorf("cannot access tag `%s` of %w", n.rhs, errUnspecifiedEntity)
826+
}
827+
828+
t, err := evalString(n.rhs, env)
829+
if err != nil {
830+
return zeroValue(), err
831+
}
832+
833+
e, ok := env.Entities.Get(eid)
834+
if !ok {
835+
return zeroValue(), fmt.Errorf("entity `%v` %w", eid.String(), errEntityNotExist)
836+
}
837+
838+
val, ok := e.Tags.Get(t)
839+
if !ok {
840+
return zeroValue(), fmt.Errorf("`%s` %w `%s`", eid.String(), errTagAccess, t)
841+
}
842+
843+
return val, nil
844+
}
845+
846+
// hasTagEval
847+
type hasTagEval struct {
848+
lhs, rhs Evaler
849+
}
850+
851+
func newHasTagEval(object, tag Evaler) *hasTagEval {
852+
return &hasTagEval{lhs: object, rhs: tag}
853+
}
854+
855+
func (n *hasTagEval) Eval(env Env) (types.Value, error) {
856+
eid, err := evalEntity(n.lhs, env)
857+
if err != nil {
858+
return zeroValue(), err
859+
}
860+
861+
t, err := evalString(n.rhs, env)
862+
if err != nil {
863+
return zeroValue(), err
864+
}
865+
866+
e, ok := env.Entities.Get(eid)
867+
if !ok {
868+
return types.False, nil
869+
}
870+
871+
_, ok = e.Tags.Get(t)
872+
return types.Boolean(ok), nil
873+
}
874+
807875
// likeEval
808876
type likeEval struct {
809877
lhs Evaler
@@ -1139,7 +1207,7 @@ func newExtensionEval(name types.Path, args []Evaler) Evaler {
11391207

11401208
if i, ok := extensions.ExtMap[name]; ok {
11411209
if i.Args != len(args) {
1142-
return newErrorEval(fmt.Errorf("%w: %s takes %d parameter(s)", errArity, name, i.Args))
1210+
return newErrorEval(fmt.Errorf("%w: %s takes %d parameter(s), but %d provided", errArity, name, i.Args, len(args)))
11431211
}
11441212
switch {
11451213
case name == "datetime":

0 commit comments

Comments
 (0)