Skip to content

Commit d24c680

Browse files
committed
Add support for multi-object checks
1 parent 6ea62b0 commit d24c680

File tree

8 files changed

+159
-63
lines changed

8 files changed

+159
-63
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,15 @@ tests:
306306
# checks can also be defined for multiple users sharing the same expectation
307307
- object: group:employees
308308
users:
309+
# checks can also target multiple objects with the same expectation
310+
- objects:
311+
- group:admins
312+
- group:employees
313+
user: user:1
314+
assertions:
315+
moderator: false
309316
# either "user" or "users" may be provided, but not both
317+
# either "object" or "objects" may be provided, but not both
310318
- user:3
311319
- user:4
312320
assertions:
@@ -585,7 +593,15 @@ tests: # required
585593
- user:carl
586594
assertions:
587595
can_view: false
596+
# checks can group multiple objects that share the same expected results
597+
- objects:
598+
- folder:1
599+
- folder:2
600+
user: user:beth
601+
assertions:
602+
can_write: false
588603
# either "user" or "users" may be provided, but not both
604+
# either "object" or "objects" may be provided, but not both
589605
list_objects: # a set of list objects to run
590606
- user: user:anne
591607
type: folder

cmd/store/import.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -237,15 +237,18 @@ func getCheckAssertions(checkTests []storetest.ModelTestCheck) []client.ClientAs
237237

238238
for _, checkTest := range checkTests {
239239
users := storetest.GetEffectiveUsers(checkTest)
240+
objects := storetest.GetEffectiveObjects(checkTest)
240241

241242
for _, user := range users {
242-
for relation, expectation := range checkTest.Assertions {
243-
assertions = append(assertions, client.ClientAssertion{
244-
User: user,
245-
Relation: relation,
246-
Object: checkTest.Object,
247-
Expectation: expectation,
248-
})
243+
for _, object := range objects {
244+
for relation, expectation := range checkTest.Assertions {
245+
assertions = append(assertions, client.ClientAssertion{
246+
User: user,
247+
Relation: relation,
248+
Object: object,
249+
Expectation: expectation,
250+
})
251+
}
249252
}
250253
}
251254
}

cmd/store/import_test.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@ func TestImportStore(t *testing.T) {
3636
Expectation: true,
3737
},
3838
}
39+
40+
multiObjectAssertions := []client.ClientAssertion{
41+
{
42+
User: "user:peter",
43+
Relation: "reader",
44+
Object: "document:doc1",
45+
Expectation: true,
46+
},
47+
{
48+
User: "user:peter",
49+
Relation: "reader",
50+
Object: "document:doc2",
51+
Expectation: true,
52+
},
53+
}
3954
modelID, storeID := "model-1", "store-1"
4055
expectedOptions := client.ClientWriteAssertionsOptions{AuthorizationModelId: &modelID, StoreId: &storeID}
4156

@@ -75,6 +90,30 @@ func TestImportStore(t *testing.T) {
7590
mockWriteAssertions: true,
7691
mockWriteModel: true,
7792
mockCreateStore: true,
93+
testStore: storetest.StoreData{
94+
Model: `type user
95+
type document
96+
relations
97+
define reader: [user]`,
98+
Tests: []storetest.ModelTest{
99+
{
100+
Name: "Test",
101+
Check: []storetest.ModelTestCheck{
102+
{
103+
Users: []string{"user:anne", "user:peter"},
104+
Object: "document:doc1",
105+
Assertions: map[string]bool{"reader": true},
106+
},
107+
},
108+
},
109+
},
110+
},
111+
},
112+
{
113+
name: "import store with multi object assertions",
114+
mockWriteAssertions: true,
115+
mockWriteModel: true,
116+
mockCreateStore: true,
78117
testStore: storetest.StoreData{
79118
Model: `type user
80119
type document
@@ -85,8 +124,8 @@ func TestImportStore(t *testing.T) {
85124
Name: "Test",
86125
Check: []storetest.ModelTestCheck{
87126
{
88-
Users: []string{"user:anne", "user:peter"},
89-
Object: "document:doc1",
127+
User: "user:peter",
128+
Objects: []string{"document:doc1", "document:doc2"},
90129
Assertions: map[string]bool{"reader": true},
91130
},
92131
},
@@ -147,8 +186,12 @@ func TestImportStore(t *testing.T) {
147186

148187
if test.mockWriteAssertions {
149188
expected := expectedAssertions
150-
if test.name == "import store with multi user assertions" {
189+
190+
switch test.name {
191+
case "import store with multi user assertions":
151192
expected = multiUserAssertions
193+
case "import store with multi object assertions":
194+
expected = multiObjectAssertions
152195
}
153196

154197
setupWriteAssertionsMock(mockCtrl, mockFgaClient, expected, expectedOptions)

internal/storetest/localtest.go

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -27,54 +27,57 @@ func RunLocalCheckTest(
2727
results := []ModelTestCheckSingleResult{}
2828
users := GetEffectiveUsers(checkTest)
2929

30+
objects := GetEffectiveObjects(checkTest)
3031
for _, user := range users {
31-
for relation, expectation := range checkTest.Assertions {
32-
result := ModelTestCheckSingleResult{
33-
Request: client.ClientCheckRequest{
34-
User: user,
35-
Relation: relation,
36-
Object: checkTest.Object,
37-
ContextualTuples: tuples,
38-
Context: checkTest.Context,
39-
},
40-
Expected: expectation,
41-
}
32+
for _, object := range objects {
33+
for relation, expectation := range checkTest.Assertions {
34+
result := ModelTestCheckSingleResult{
35+
Request: client.ClientCheckRequest{
36+
User: user,
37+
Relation: relation,
38+
Object: object,
39+
ContextualTuples: tuples,
40+
Context: checkTest.Context,
41+
},
42+
Expected: expectation,
43+
}
4244

43-
var (
44-
ctx *structpb.Struct
45-
err error
46-
)
45+
var (
46+
ctx *structpb.Struct
47+
err error
48+
)
4749

48-
if checkTest.Context != nil {
49-
ctx, err = structpb.NewStruct(*checkTest.Context)
50-
}
50+
if checkTest.Context != nil {
51+
ctx, err = structpb.NewStruct(*checkTest.Context)
52+
}
5153

52-
if err != nil {
53-
result.Error = err
54-
} else {
55-
response, err := RunSingleLocalCheckTest(fgaServer,
56-
&pb.CheckRequest{
57-
StoreId: *options.StoreID,
58-
AuthorizationModelId: *options.ModelID,
59-
TupleKey: &pb.CheckRequestTupleKey{
60-
User: user,
61-
Relation: relation,
62-
Object: checkTest.Object,
63-
},
64-
Context: ctx,
65-
},
66-
)
6754
if err != nil {
6855
result.Error = err
56+
} else {
57+
response, err := RunSingleLocalCheckTest(fgaServer,
58+
&pb.CheckRequest{
59+
StoreId: *options.StoreID,
60+
AuthorizationModelId: *options.ModelID,
61+
TupleKey: &pb.CheckRequestTupleKey{
62+
User: user,
63+
Relation: relation,
64+
Object: object,
65+
},
66+
Context: ctx,
67+
},
68+
)
69+
if err != nil {
70+
result.Error = err
71+
}
72+
73+
if response != nil {
74+
result.Got = &response.Allowed
75+
result.TestResult = result.IsPassing()
76+
}
6977
}
7078

71-
if response != nil {
72-
result.Got = &response.Allowed
73-
result.TestResult = result.IsPassing()
74-
}
79+
results = append(results, result)
7580
}
76-
77-
results = append(results, result)
7881
}
7982
}
8083

internal/storetest/remotetest.go

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,24 @@ func RunRemoteCheckTest(
3535
results := []ModelTestCheckSingleResult{}
3636

3737
users := GetEffectiveUsers(checkTest)
38+
objects := GetEffectiveObjects(checkTest)
3839

3940
for _, user := range users {
40-
for relation, expectation := range checkTest.Assertions {
41-
result := RunSingleRemoteCheckTest(
42-
fgaClient,
43-
client.ClientCheckRequest{
44-
User: user,
45-
Relation: relation,
46-
Object: checkTest.Object,
47-
Context: checkTest.Context,
48-
ContextualTuples: tuples,
49-
},
50-
expectation,
51-
)
52-
results = append(results, result)
41+
for _, object := range objects {
42+
for relation, expectation := range checkTest.Assertions {
43+
result := RunSingleRemoteCheckTest(
44+
fgaClient,
45+
client.ClientCheckRequest{
46+
User: user,
47+
Relation: relation,
48+
Object: object,
49+
Context: checkTest.Context,
50+
ContextualTuples: tuples,
51+
},
52+
expectation,
53+
)
54+
results = append(results, result)
55+
}
5356
}
5457
}
5558

internal/storetest/storedata.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type ModelTestCheck struct {
3434
User string `json:"user" yaml:"user"`
3535
Users []string `json:"users" yaml:"users"`
3636
Object string `json:"object" yaml:"object"`
37+
Objects []string `json:"objects" yaml:"objects"`
3738
Context *map[string]interface{} `json:"context" yaml:"context,omitempty"`
3839
Assertions map[string]bool `json:"assertions" yaml:"assertions"`
3940
}
@@ -140,6 +141,7 @@ func (storeData *StoreData) LoadTuples(basePath string) error {
140141
return nil
141142
}
142143

144+
//nolint:cyclop
143145
func (storeData *StoreData) Validate() error {
144146
var errs error
145147

@@ -152,6 +154,14 @@ func (storeData *StoreData) Validate() error {
152154
msg := fmt.Sprintf("test %s check %d must specify 'user' or 'users'", test.Name, index)
153155
errs = errors.Join(errs, fmt.Errorf("%w: %s", clierrors.ErrValidation, msg))
154156
}
157+
158+
if check.Object != "" && len(check.Objects) > 0 {
159+
msg := fmt.Sprintf("test %s check %d cannot contain both 'object' and 'objects'", test.Name, index)
160+
errs = errors.Join(errs, fmt.Errorf("%w: %s", clierrors.ErrValidation, msg))
161+
} else if check.Object == "" && len(check.Objects) == 0 {
162+
msg := fmt.Sprintf("test %s check %d must specify 'object' or 'objects'", test.Name, index)
163+
errs = errors.Join(errs, fmt.Errorf("%w: %s", clierrors.ErrValidation, msg))
164+
}
155165
}
156166
}
157167

internal/storetest/storedata_test.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,23 @@ func TestStoreDataValidate(t *testing.T) {
2020
}}}}}
2121
assert.NoError(t, validUsers.Validate())
2222

23+
validObjects := StoreData{Tests: []ModelTest{{Name: "t1", Check: []ModelTestCheck{{
24+
User: "user:1", Objects: []string{"doc:1", "doc:2"}, Assertions: map[string]bool{"read": true},
25+
}}}}}
26+
assert.NoError(t, validObjects.Validate())
27+
2328
invalidBoth := StoreData{Tests: []ModelTest{{Name: "t1", Check: []ModelTestCheck{{
2429
User: "user:1", Users: []string{"user:2"}, Object: "doc:1", Assertions: map[string]bool{"read": true},
2530
}}}}}
2631
require.Error(t, invalidBoth.Validate())
2732

33+
invalidObjectBoth := StoreData{Tests: []ModelTest{{Name: "t1", Check: []ModelTestCheck{{
34+
User: "user:1", Object: "doc:1", Objects: []string{"doc:2"}, Assertions: map[string]bool{"read": true},
35+
}}}}}
36+
require.Error(t, invalidObjectBoth.Validate())
37+
2838
invalidNone := StoreData{Tests: []ModelTest{{Name: "t1", Check: []ModelTestCheck{{
29-
Object: "doc:1", Assertions: map[string]bool{"read": true},
39+
User: "user:1", Assertions: map[string]bool{"read": true},
3040
}}}}}
3141
require.Error(t, invalidNone.Validate())
3242
}

internal/storetest/utils.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,11 @@ func GetEffectiveUsers(checkTest ModelTestCheck) []string {
77

88
return []string{checkTest.User}
99
}
10+
11+
func GetEffectiveObjects(checkTest ModelTestCheck) []string {
12+
if len(checkTest.Objects) > 0 {
13+
return checkTest.Objects
14+
}
15+
16+
return []string{checkTest.Object}
17+
}

0 commit comments

Comments
 (0)