Skip to content

Commit 9d7b6c4

Browse files
authored
feat: support grouping user/object checks in store tests (#513)
* Add users list support in store check tests * fix lint * chore: fixed example * refactor: consolidate user selection logic * refactor: centralize getEffectiveUsers * Validate check definitions and add tests * Add support for multi-object checks * fix: example was not working * chore: added example for multiple objects * chore: simplified error messages * fix: fixed export command to not to write empty users/objects * fix: linter issues * chore: removed unneeded file and extra whitespaces in .fga.yaml * chore: improved example * chore: removed trailing whitespace
1 parent be43c33 commit 9d7b6c4

13 files changed

Lines changed: 400 additions & 89 deletions

README.md

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,22 @@ tests:
303303
assertions:
304304
member: true
305305
moderator: false
306+
# checks can also be defined for multiple users sharing the same expectation
307+
- object: group:employees
308+
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
316+
# either "user" or "users" may be provided, but not both
317+
# either "object" or "objects" may be provided, but not both
318+
- user:3
319+
- user:4
320+
assertions:
321+
member: true
306322
```
307323
308324
If using `output-file`, the response will be written to the specified file on disk. If the desired file already exists, you will be prompted to overwrite the file.
@@ -558,19 +574,35 @@ tests: # required
558574
- name: test-1
559575
description: testing that the model works # optional
560576
# tuple_file: ./tuples.json # tuples that would apply per test
561-
tuples:
562-
- user: user:anne
563-
relation: owner
564-
object: folder:1
565-
check: # a set of checks to run
566-
- user: user:anne
567-
object: folder:1
568-
assertions:
569-
# a set of expected results for each relation
570-
can_view: true
571-
can_write: true
572-
can_share: false
573-
list_objects: # a set of list objects to run
577+
tuples:
578+
- user: user:anne
579+
relation: owner
580+
object: folder:1
581+
check: # a set of checks to run
582+
- user: user:anne
583+
object: folder:1
584+
assertions:
585+
# a set of expected results for each relation
586+
can_view: true
587+
can_write: true
588+
can_share: false
589+
# checks can group multiple users that share the same expected results
590+
- object: folder:2
591+
users:
592+
- user:beth
593+
- user:carl
594+
assertions:
595+
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
603+
# either "user" or "users" may be provided, but not both
604+
# either "object" or "objects" may be provided, but not both
605+
list_objects: # a set of list objects to run
574606
- user: user:anne
575607
type: folder
576608
assertions:

cmd/store/import.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ func importStore(
121121
maxTuplesPerWrite, maxParallelRequests int,
122122
fileName string,
123123
) (*CreateStoreAndModelResponse, error) {
124+
if err := storeData.Validate(); err != nil {
125+
return nil, err //nolint:wrapcheck
126+
}
127+
124128
response, err := createOrUpdateStore(ctx, clientConfig, fgaClient, storeData, format, storeID, fileName)
125129
if err != nil {
126130
return nil, err
@@ -232,13 +236,20 @@ func getCheckAssertions(checkTests []storetest.ModelTestCheck) []client.ClientAs
232236
var assertions []client.ClientAssertion
233237

234238
for _, checkTest := range checkTests {
235-
for relation, expectation := range checkTest.Assertions {
236-
assertions = append(assertions, client.ClientAssertion{
237-
User: checkTest.User,
238-
Relation: relation,
239-
Object: checkTest.Object,
240-
Expectation: expectation,
241-
})
239+
users := storetest.GetEffectiveUsers(checkTest)
240+
objects := storetest.GetEffectiveObjects(checkTest)
241+
242+
for _, user := range users {
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+
}
252+
}
242253
}
243254
}
244255

cmd/store/import_test.go

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,36 @@ func TestImportStore(t *testing.T) {
2121
Object: "document:doc1",
2222
Expectation: true,
2323
}}
24+
25+
multiUserAssertions := []client.ClientAssertion{
26+
{
27+
User: "user:anne",
28+
Relation: "reader",
29+
Object: "document:doc1",
30+
Expectation: true,
31+
},
32+
{
33+
User: "user:peter",
34+
Relation: "reader",
35+
Object: "document:doc1",
36+
Expectation: true,
37+
},
38+
}
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+
}
2454
modelID, storeID := "model-1", "store-1"
2555
expectedOptions := client.ClientWriteAssertionsOptions{AuthorizationModelId: &modelID, StoreId: &storeID}
2656

@@ -38,9 +68,9 @@ func TestImportStore(t *testing.T) {
3868
mockCreateStore: true,
3969
testStore: storetest.StoreData{
4070
Model: `type user
41-
type document
42-
relations
43-
define reader: [user]`,
71+
type document
72+
relations
73+
define reader: [user]`,
4474
Tests: []storetest.ModelTest{
4575
{
4676
Name: "Test",
@@ -55,6 +85,54 @@ func TestImportStore(t *testing.T) {
5585
},
5686
},
5787
},
88+
{
89+
name: "import store with multi user assertions",
90+
mockWriteAssertions: true,
91+
mockWriteModel: true,
92+
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,
117+
testStore: storetest.StoreData{
118+
Model: `type user
119+
type document
120+
relations
121+
define reader: [user]`,
122+
Tests: []storetest.ModelTest{
123+
{
124+
Name: "Test",
125+
Check: []storetest.ModelTestCheck{
126+
{
127+
User: "user:peter",
128+
Objects: []string{"document:doc1", "document:doc2"},
129+
Assertions: map[string]bool{"reader": true},
130+
},
131+
},
132+
},
133+
},
134+
},
135+
},
58136
{
59137
name: "create new store without assertions",
60138
mockWriteAssertions: false,
@@ -107,7 +185,16 @@ func TestImportStore(t *testing.T) {
107185
defer mockCtrl.Finish()
108186

109187
if test.mockWriteAssertions {
110-
setupWriteAssertionsMock(mockCtrl, mockFgaClient, expectedAssertions, expectedOptions)
188+
expected := expectedAssertions
189+
190+
switch test.name {
191+
case "import store with multi user assertions":
192+
expected = multiUserAssertions
193+
case "import store with multi object assertions":
194+
expected = multiObjectAssertions
195+
}
196+
197+
setupWriteAssertionsMock(mockCtrl, mockFgaClient, expected, expectedOptions)
111198
} else {
112199
mockFgaClient.EXPECT().WriteAssertions(gomock.Any()).Times(0)
113200
}

example/folder-document-access_tuples.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9084,11 +9084,6 @@
90849084
"relation": "can_view",
90859085
"user": "user:58"
90869086
},
9087-
{
9088-
"object": "document:578",
9089-
"relation": "can_view",
9090-
"user": "user:410"
9091-
},
90929087
{
90939088
"object": "document:25",
90949089
"relation": "can_view",

example/model.fga

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ type document
2121
define viewer: [user] or owner or parent_owner
2222
define can_share: owner
2323
define can_write: owner or parent_owner
24-
define can_view: viewer or viewer from parent
24+
define can_view: [user] or viewer or viewer from parent

example/model.fga.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,33 @@ tests:
4747
can_view: true
4848
can_write: true
4949
can_share: false
50+
- users:
51+
- user:anne
52+
- user:beth
53+
object: folder:product-2021
54+
assertions:
55+
# These assertions are run for each user
56+
can_view: true
57+
can_share: false
58+
- user: user:anne
59+
objects:
60+
- folder:product-2021
61+
- folder:product-2021Q1
62+
assertions:
63+
# These assertions are run for each object
64+
can_view: true
65+
can_write: true
66+
- users:
67+
- user:peter
68+
- user:john
69+
objects:
70+
- folder:product-2021
71+
- folder:product-2021Q1
72+
assertions:
73+
# These assertions are run for each user and each object
74+
can_view: false
75+
can_write: false
76+
5077
list_objects: # Each list objects test is made of: a user, an object type and the expected result for one or more relations
5178
- user: user:anne
5279
type: folder
@@ -69,6 +96,7 @@ tests:
6996
- folder:product-2021Q1
7097
can_write: []
7198
can_share: []
99+
72100
list_users: # Each list user test is made of: an object, a user filter and the expected result for one or more relations
73101
- object: folder:product-2021
74102
user_filter:

internal/storetest/localtest.go

Lines changed: 48 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -25,54 +25,60 @@ func RunLocalCheckTest(
2525
options ModelTestOptions,
2626
) []ModelTestCheckSingleResult {
2727
results := []ModelTestCheckSingleResult{}
28+
users := GetEffectiveUsers(checkTest)
29+
30+
objects := GetEffectiveObjects(checkTest)
31+
for _, user := range users {
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+
}
2844

29-
for relation, expectation := range checkTest.Assertions {
30-
result := ModelTestCheckSingleResult{
31-
Request: client.ClientCheckRequest{
32-
User: checkTest.User,
33-
Relation: relation,
34-
Object: checkTest.Object,
35-
ContextualTuples: tuples,
36-
Context: checkTest.Context,
37-
},
38-
Expected: expectation,
39-
}
45+
var (
46+
ctx *structpb.Struct
47+
err error
48+
)
4049

41-
var (
42-
ctx *structpb.Struct
43-
err error
44-
)
45-
46-
if checkTest.Context != nil {
47-
ctx, err = structpb.NewStruct(*checkTest.Context)
48-
}
50+
if checkTest.Context != nil {
51+
ctx, err = structpb.NewStruct(*checkTest.Context)
52+
}
4953

50-
if err != nil {
51-
result.Error = err
52-
} else {
53-
response, err := RunSingleLocalCheckTest(fgaServer,
54-
&pb.CheckRequest{
55-
StoreId: *options.StoreID,
56-
AuthorizationModelId: *options.ModelID,
57-
TupleKey: &pb.CheckRequestTupleKey{
58-
User: checkTest.User,
59-
Relation: relation,
60-
Object: checkTest.Object,
61-
},
62-
Context: ctx,
63-
},
64-
)
65-
if err != nil {
66-
result.Error = err
67-
}
54+
if err != nil {
55+
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+
}
77+
}
6878

69-
if response != nil {
70-
result.Got = &response.Allowed
71-
result.TestResult = result.IsPassing()
79+
results = append(results, result)
7280
}
7381
}
74-
75-
results = append(results, result)
7682
}
7783

7884
return results

0 commit comments

Comments
 (0)