Skip to content

Commit

Permalink
feat(model): run tests against a built-in OpenFGA engine (#142)
Browse files Browse the repository at this point in the history
  • Loading branch information
rhamzeh authored Sep 6, 2023
2 parents 208d7ef + f2e3968 commit aedcff0
Show file tree
Hide file tree
Showing 18 changed files with 1,065 additions and 434 deletions.
103 changes: 66 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,46 +432,75 @@ fga model **test**
* `--tests`: Name of the tests file. Must be in yaml format (see below)
* `--verbose`: Outputs the results in JSON

The test file should have the following format:
If a model is provided, the test will run in a built-in OpenFGA instance (you do not need a separate server). Otherwise, the test will be run against the configured store of your OpenFGA instance. When running against a remote instance, the tuples will be sent as contextual tuples, and will have to abide by the OpenFGA server limits (20 contextual tuples per request).

The tests file should be in yaml and have the following format:

```yaml
---
- name: some-test
description: testing that the model works
tuples:
- user: user:anne
relation: owner
object: folder:product
check:
- user: user:anne
object: folder:product-2021
assertions:
# a set of expected results for each relation
can_view: true
can_write: false
can_share: false
list-objects:
- user: user:anne
type: folder
assertions:
# a set of expected results for each relation
can_view:
- folder:product
- folder:product-2021
can_write:
- folder:product
- folder:product-2021
can_share:
- folder:product
- folder:product-2021
- user: user:beth
type: folder
assertions:
# a set of expected results for each relation
can_view:
- folder:product-2021
can_write: []
can_share: []
name: Store Name # store name, optional
# model_file: ./model.fga # a global model that would apply to all tests, optional
# model can be used instead of model-file, optional
model: |
model
schema 1.1
type user
type folder
relations
define owner: [user] or owner
define parent: [folder]
define can_view: owner
tuples: # global tuples that would apply to all tests, optional
- user: folder:1
relation: parent
object: folder:2
tests: # required
- name: test-1
description: testing that the model works # optional
tuples:
- user: user:anne
relation: owner
object: folder:1
check: # a set of checks to run
- user: user:anne
object: folder:1
assertions:
# a set of expected results for each relation
can_view: true
can_write: true
can_share: false
list_objects: # a set of list objects to run
- user: user:anne
type: folder
assertions:
# a set of expected results for each relation
can_view:
- folder:1
- folder:2
can_write:
- folder:1
- folder:2
can_share: []
- name: test-2
description: another test
tuples:
- user: user:anne
relation: owner
object: folder:1
check:
- user: user:anne
object: folder:1
assertions:
# a set of expected results for each relation
can_view: true
list_objects:
- user: user:anne
type: folder
assertions:
# a set of expected results for each relation
can_view:
- folder:1
- folder:2
```
###### Example
Expand Down
34 changes: 20 additions & 14 deletions cmd/model/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ package model
import (
"fmt"
"os"
"path"
"strings"

"github.com/openfga/cli/internal/authorizationmodel"
"github.com/openfga/cli/internal/cmdutils"
"github.com/openfga/cli/internal/output"
"github.com/openfga/cli/internal/storetest"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
Expand All @@ -33,7 +34,7 @@ var testCmd = &cobra.Command{
Use: "test",
Short: "Test an Authorization Model",
Long: "Run a set of tests against a particular Authorization Model.",
Example: `fga model test --file model.fga --tests tests.fga.yaml`,
Example: `fga model test --tests tests.fga.yaml`,
RunE: func(cmd *cobra.Command, args []string) error {
clientConfig := cmdutils.GetClientConfig(cmd)

Expand All @@ -47,23 +48,33 @@ var testCmd = &cobra.Command{
return err //nolint:wrapcheck
}

testFileContents, err := os.ReadFile(testsFileName)
var storeData storetest.StoreData

testFile, err := os.Open(testsFileName)
if err != nil {
return fmt.Errorf("failed to read file %s due to %w", testsFileName, err)
}

var tests []authorizationmodel.ModelTest
if err := yaml.Unmarshal(testFileContents, &tests); err != nil {
return err //nolint:wrapcheck
decoder := yaml.NewDecoder(testFile)
decoder.KnownFields(true)
err = decoder.Decode(&storeData)
if err != nil {
return fmt.Errorf("failed to unmarshal file %s due to %w", testsFileName, err)
}

results := authorizationmodel.RunTests(fgaClient, tests)

verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
return err //nolint:wrapcheck
}

results, err := storetest.RunTests(
fgaClient,
storeData,
path.Dir(testsFileName),
)
if err != nil {
return fmt.Errorf("error running tests due to %w", err)
}

if verbose {
return output.Display(results) //nolint:wrapcheck
}
Expand All @@ -86,11 +97,6 @@ func init() {
testCmd.Flags().String("tests", "", "Tests file Name. The file should have the OpenFGA tests in a valid YAML or JSON format") //nolint:lll
testCmd.Flags().Bool("verbose", false, "Print verbose JSON output")

if err := testCmd.MarkFlagRequired("store-id"); err != nil {
fmt.Printf("error setting flag as required - %v: %v\n", "cmd/models/test", err)
os.Exit(1)
}

if err := testCmd.MarkFlagRequired("tests"); err != nil {
fmt.Printf("error setting flag as required - %v: %v\n", "cmd/models/test", err)
os.Exit(1)
Expand Down
8 changes: 4 additions & 4 deletions cmd/model/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ fga model transform '{ "schema_version": "1.1", "type_definitions":[{"type":"use
}

authModel := authorizationmodel.AuthzModel{}
if transformInputFormat == authorizationmodel.ModelFormatJSON {
if err := authModel.ReadFromJSONString(inputModel); err != nil {
return err //nolint:wrapcheck
}
if err := authModel.ReadModelFromString(inputModel, transformInputFormat); err != nil {
return err //nolint:wrapcheck
}

if transformInputFormat == authorizationmodel.ModelFormatJSON {
dslModel, err := authModel.DisplayAsDSL([]string{"model"})
if err != nil {
return fmt.Errorf("failed to transform model due to %w", err)
Expand Down
4 changes: 2 additions & 2 deletions example/model.fga
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type folder
define parent: [folder]
define owner: [user]
define parent_owner: owner from parent or parent_owner from parent
define viewer: [user] or owner
define viewer: [user] or owner or parent_owner
define can_share: owner
define can_write: owner or parent_owner
define can_view: viewer
define can_view: viewer or viewer from parent
115 changes: 69 additions & 46 deletions example/tests.fga.yaml
Original file line number Diff line number Diff line change
@@ -1,46 +1,69 @@
---
- name: "folder-document-access"
description: ""
tuples:
- user: user:anne
relation: owner
object: folder:product
- user: folder:product
relation: parent
object: folder:product-2021
- user: user:beth
relation: viewer
object: folder:product-2021
check:
- user: user:anne
object: folder:product-2021
assertions:
can_view: true
can_write: true
can_share: true
- user: user:beth
object: folder:product-2021
assertions:
can_view: true
can_write: false
can_share: false
list-objects:
- user: user:anne
type: folder
assertions:
can_view:
- folder:product
- folder:product-2021
can_write:
- folder:product
- folder:product-2021
can_share:
- folder:product
- folder:product-2021
- user: user:beth
type: folder
assertions:
can_view:
- folder:product-2021
can_write: []
can_share: []
name: FolderBox # store name
model_file: ./model.fga # a global model that would apply to all tests
# model can be used instead of model_file
#model: |
# model
# schema 1.1
# type user
# ...
tuples: # global tuples that would apply to all tests
- user: folder:5
relation: parent
object: folder:product-2021
- user: folder:product-2021
relation: parent
object: folder:product-2021Q1
tests:
- name: "folder-document-access"
description: ""
tuples: # tuples in tests are appended to the global tuples and do not replace them
- user: user:anne
relation: owner
object: folder:product
- user: folder:product
relation: parent
object: folder:product-2021
- user: user:beth
relation: viewer
object: folder:product-2021
check: # Each check test is made of: a user, an object and the expected result for one or more relations
- user: user:anne
object: folder:product-2021
assertions:
can_view: true
can_write: true
can_share: false
- user: user:beth
object: folder:product-2021
assertions:
can_view: true
can_write: false
can_share: false
- user: user:anne
object: folder:product-2021Q1
assertions:
can_view: true
can_write: true
can_share: false
list_objects: # Each check test is made of: a user, an object type and the expected result for one or more relations
- user: user:anne
type: folder
assertions:
can_view:
- folder:product
- folder:product-2021
- folder:product-2021Q1
can_write:
- folder:product
- folder:product-2021
- folder:product-2021Q1
can_share:
- folder:product
- user: user:beth
type: folder
assertions:
can_view:
- folder:product-2021
- folder:product-2021Q1
can_write: []
can_share: []
19 changes: 18 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/openfga/api/proto v0.0.0-20230801154117-db20ad164368
github.com/openfga/go-sdk v0.2.3-0.20230710203920-f6922b2d8c6d
github.com/openfga/language/pkg/go v0.0.0-20230823153854-0351dba7a7a3
github.com/openfga/openfga v1.3.1
github.com/openfga/openfga v1.3.2-0.20230830154907-0a84f51ac01f
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.16.0
Expand All @@ -22,12 +22,16 @@ require (

require (
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.17.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
Expand All @@ -36,17 +40,30 @@ require (
github.com/karlseguin/ccache/v3 v3.0.3 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/muesli/mango v0.2.0 // indirect
github.com/muesli/mango-pflag v0.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 // indirect
go.opentelemetry.io/otel/metric v1.16.0 // indirect
go.opentelemetry.io/otel/sdk v1.16.0 // indirect
go.opentelemetry.io/otel/trace v1.16.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sync v0.3.0 // indirect
Expand Down
Loading

0 comments on commit aedcff0

Please sign in to comment.