diff --git a/README.md b/README.md index 38a0bb7..05110d2 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cmd/model/test.go b/cmd/model/test.go index 598afd6..4e81354 100644 --- a/cmd/model/test.go +++ b/cmd/model/test.go @@ -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" ) @@ -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) @@ -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 } @@ -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) diff --git a/cmd/model/transform.go b/cmd/model/transform.go index 7f0da20..7089594 100644 --- a/cmd/model/transform.go +++ b/cmd/model/transform.go @@ -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) diff --git a/example/model.fga b/example/model.fga index ca038ea..73bd03c 100644 --- a/example/model.fga +++ b/example/model.fga @@ -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 diff --git a/example/tests.fga.yaml b/example/tests.fga.yaml index 761d8e5..b2db742 100644 --- a/example/tests.fga.yaml +++ b/example/tests.fga.yaml @@ -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: [] \ No newline at end of file +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: [] \ No newline at end of file diff --git a/go.mod b/go.mod index 6122ed7..780dc32 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 @@ -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 diff --git a/go.sum b/go.sum index 64fb240..759afca 100644 --- a/go.sum +++ b/go.sum @@ -38,9 +38,21 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -55,6 +67,14 @@ github.com/craigpastro/openfga-dsl-parser/v2 v2.0.1/go.mod h1:GG/ijmrM59umGaKjAa github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY= +github.com/docker/docker v24.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -73,11 +93,18 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -144,6 +171,8 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/grpc-gateway/v2 v2.17.1 h1:LSsiG61v9IzzxMkqEr6nrix4miJI62xlRjwT7BYD2SM= github.com/grpc-ecosystem/grpc-gateway/v2 v2.17.1/go.mod h1:Hbb13e3/WtqQ8U5hLGkek9gJvBLasHuPFI0UEGfnQ10= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -159,13 +188,21 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= +github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/karlseguin/ccache/v3 v3.0.3 h1:cz+3tSdTrovp00xHPP3Y6ca/YuSl5kchhYG83wUPYN0= github.com/karlseguin/ccache/v3 v3.0.3/go.mod h1:qxC372+Qn+IBj8Pe3KvGjHPj0sWwEF7AeZVhsNPZ6uY= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -174,6 +211,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -184,6 +225,8 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ= @@ -198,26 +241,46 @@ github.com/nwidger/jsoncolor v0.3.2 h1:rVJJlwAWDJShnbTYOQ5RM7yTA20INyKXlJ/fg4JMh github.com/nwidger/jsoncolor v0.3.2/go.mod h1:Cs34umxLbJvgBMnVNVqhji9BhoT/N/KinHqZptQ7cf4= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8= +github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= github.com/openfga/api/proto v0.0.0-20230801154117-db20ad164368 h1:xva1rkYhP0/Ocyg3O2RyeFj7vOa7UCKcuifUg+VmrNg= github.com/openfga/api/proto v0.0.0-20230801154117-db20ad164368/go.mod h1:/edBiulCLBxFw+Q1wfWyURo/grVjueLo6JWFPHHFTos= github.com/openfga/go-sdk v0.2.3-0.20230710203920-f6922b2d8c6d h1:xK4EfSnsB+U8zLyZ05h8V9omL6LF6Xh763bQphghuLk= github.com/openfga/go-sdk v0.2.3-0.20230710203920-f6922b2d8c6d/go.mod h1:ZB13O8GilPc0ITWssOszgxmz6CnIe8PQLZqbqAnx2IY= github.com/openfga/language/pkg/go v0.0.0-20230823153854-0351dba7a7a3 h1:ZKazvF9cmtIV7X8unRL1nWYy+MKeTF/RY57znuyLYU4= github.com/openfga/language/pkg/go v0.0.0-20230823153854-0351dba7a7a3/go.mod h1:gU/4rU1D6bUxYkb4FofkwcmFaavpd2h3U2+x2qAeUnY= -github.com/openfga/openfga v1.3.1 h1:mcsPUQbv97iCqrzOqGZV+Y1g8kzSKdz5qW8d+xMroF8= -github.com/openfga/openfga v1.3.1/go.mod h1:S6I+CD5VeZf6yHRDqZRkOgmKswmVFim6jMvtYzih2Fs= +github.com/openfga/openfga v1.3.2-0.20230828211348-ca86c92f3dc8 h1:ixfq/rhv5SLBmiQGp/3A3F8jqCY/cY7TjVRhG72noo4= +github.com/openfga/openfga v1.3.2-0.20230828211348-ca86c92f3dc8/go.mod h1:sotnTS8L+9/a5HRN/4Z646FVfU5fF42JIC1Is7xdtog= +github.com/openfga/openfga v1.3.2-0.20230830154907-0a84f51ac01f h1:a+TYFVNQsPI+pJaFDID9e+O2XkcK8EPdVGpvhSzCyao= +github.com/openfga/openfga v1.3.2-0.20230830154907-0a84f51ac01f/go.mod h1:sotnTS8L+9/a5HRN/4Z646FVfU5fF42JIC1Is7xdtog= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pressly/goose/v3 v3.11.2 h1:QgTP45FhBBHdmf7hWKlbWFHtwPtxo0phSDkwDKGUrYs= +github.com/pressly/goose/v3 v3.11.2/go.mod h1:LWQzSc4vwfHA/3B8getTp8g3J5Z8tFBxgxinmGlMlJk= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= @@ -231,9 +294,11 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -256,14 +321,32 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 h1:t4ZwRPU+emrcvM2e9DHd0Fsf0JTPVcbfa/BhTDF03d0= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0/go.mod h1:vLarbg68dH2Wa77g71zmKQqlQ8+8Rq3GRG31uc0WcWI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 h1:cbsD4cUcviQGXdw8+bo5x2wazq10SKz8hEbtCRPcU78= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0/go.mod h1:JgXSGah17croqhJfhByOLVY719k1emAXC8MVhCIJlRs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 h1:TVQp/bboR4mhZSav+MdgXB8FaRho1RC8UwVn3T0vjVc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0/go.mod h1:I33vtIe0sR96wfrUcilIzLoA3mLHhRmz9S9Te0S3gDo= go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= +go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= +go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4= go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -271,6 +354,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -307,6 +392,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -369,6 +456,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -403,6 +491,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -437,6 +526,7 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -459,6 +549,7 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -467,9 +558,12 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E= +golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -521,6 +615,7 @@ google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= @@ -582,7 +677,9 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/authorizationmodel/model.go b/internal/authorizationmodel/model.go index 9b3e0eb..3cd62f9 100644 --- a/internal/authorizationmodel/model.go +++ b/internal/authorizationmodel/model.go @@ -81,6 +81,25 @@ func (model *AuthzModel) GetTypeDefinitions() []openfga.TypeDefinition { return *model.TypeDefinitions } +func (model *AuthzModel) GetProtoModel() *pb.AuthorizationModel { + if model == nil { + return nil + } + + var pbModel pb.AuthorizationModel + + jsonModel, err := model.GetAsJSONString() + if err != nil { + return nil + } + + if err = protojson.Unmarshal([]byte(*jsonModel), &pbModel); err != nil { + return nil + } + + return &pbModel +} + func (model *AuthzModel) GetCreatedAt() *time.Time { if model == nil { return nil @@ -133,14 +152,38 @@ func (model *AuthzModel) ReadFromDSLString(dslString string) error { return fmt.Errorf("failed to transform due to %w", err) } - jsonAuthModel := &openfga.AuthorizationModel{} + jsonAuthModel := openfga.AuthorizationModel{} - err = json.Unmarshal(bytes, jsonAuthModel) + err = json.Unmarshal(bytes, &jsonAuthModel) if err != nil { return fmt.Errorf("failed to transform due to %w", err) } - model.Set(*jsonAuthModel) + model.Set(jsonAuthModel) + + return nil +} + +func (model *AuthzModel) ReadModelFromString(input string, format ModelFormat) error { + if input == "" { + return nil + } + + switch format { + case ModelFormatJSON: + if err := model.ReadFromJSONString(input); err != nil { + return err + } + + return nil + case ModelFormatFGA: + case ModelFormatDefault: + if err := model.ReadFromDSLString(input); err != nil { + return err + } + + return nil + } return nil } diff --git a/internal/authorizationmodel/read-from-input.go b/internal/authorizationmodel/read-from-input.go index db2e07b..2105c14 100644 --- a/internal/authorizationmodel/read-from-input.go +++ b/internal/authorizationmodel/read-from-input.go @@ -26,7 +26,36 @@ import ( "github.com/spf13/cobra" ) -func ReadFromInputFileOrArg( //nolint:cyclop +func ReadFromFile( + fileName string, + input *string, + format *ModelFormat, + storeName *string, +) error { + file, err := os.ReadFile(fileName) + if err != nil { + return fmt.Errorf("failed to read file %s due to %w", fileName, err) + } + + *input = string(file) + + // if the input format is set as the default, set it from the file extension (and default to fga) + if *format == ModelFormatDefault { + if strings.HasSuffix(fileName, "json") { + *format = ModelFormatJSON + } else { + *format = ModelFormatFGA + } + } + + if *storeName == "" { + *storeName = strings.TrimSuffix(path.Base(fileName), filepath.Ext(fileName)) + } + + return nil +} + +func ReadFromInputFileOrArg( cmd *cobra.Command, args []string, fileNameArg string, @@ -42,24 +71,8 @@ func ReadFromInputFileOrArg( //nolint:cyclop switch { case fileName != "": - file, err := os.ReadFile(fileName) - if err != nil { - return fmt.Errorf("failed to read file %s due to %w", fileName, err) - } - - *input = string(file) - - // if the input format is set as the default, set it from the file extension (and default to fga) - if *format == ModelFormatDefault { - if strings.HasSuffix(fileName, "json") { - *format = ModelFormatJSON - } else { - *format = ModelFormatFGA - } - } - - if *storeName == "" { - *storeName = strings.TrimSuffix(path.Base(fileName), filepath.Ext(fileName)) + if err = ReadFromFile(fileName, input, format, storeName); err != nil { + return err } case len(args) > 0 && args[0] != "-": *input = args[0] diff --git a/internal/authorizationmodel/tests.go b/internal/authorizationmodel/tests.go deleted file mode 100644 index 4b3e503..0000000 --- a/internal/authorizationmodel/tests.go +++ /dev/null @@ -1,306 +0,0 @@ -package authorizationmodel - -import ( - "context" - "fmt" - "sort" - - "github.com/openfga/go-sdk/client" -) - -func checkStringArraysEqual(array1 []string, array2 []string) bool { - if len(array1) != len(array2) { - return false - } - - sort.Strings(array1) - sort.Strings(array2) - - for index, value := range array1 { - if value != array2[index] { - return false - } - } - - return true -} - -type ModelTestCheckSingleResult struct { - Request client.ClientCheckRequest `json:"request"` - Expected bool `json:"expected"` - Got *bool `json:"got"` - Error error `json:"error"` - TestResult bool `json:"test_result"` -} - -func (result ModelTestCheckSingleResult) IsPassing() bool { - return result.Error == nil && *result.Got == result.Expected -} - -type ModelTestListObjectsSingleResult struct { - Request client.ClientListObjectsRequest `json:"request"` - Expected []string `json:"expected"` - Got *[]string `json:"got"` - Error error `json:"error"` - TestResult bool `json:"test_result"` -} - -func (result ModelTestListObjectsSingleResult) IsPassing() bool { - return result.Error == nil && checkStringArraysEqual(*result.Got, result.Expected) -} - -type TestResult struct { - Name string `json:"name"` - Description string `json:"description"` - CheckResults []ModelTestCheckSingleResult `json:"check_results"` - ListObjectsResults []ModelTestListObjectsSingleResult `json:"list_objects_results"` -} - -//nolint:cyclop -func (result TestResult) FriendlyDisplay() string { - totalCheckCount := len(result.CheckResults) - failedCheckCount := 0 - totalListObjectsCount := len(result.ListObjectsResults) - failedListObjectsCount := 0 - checkResultsOutput := "" - listObjectsResultsOutput := "" - - if totalCheckCount > 0 { - for index := 0; index < totalCheckCount; index++ { - checkResult := result.CheckResults[index] - - if result.CheckResults[index].IsPassing() { - checkResultsOutput = fmt.Sprintf( - "%s\n✓ Check(user=%s,relation=%s,object=%s)", - checkResultsOutput, - checkResult.Request.User, - checkResult.Request.Relation, - checkResult.Request.Object, - ) - } else { - failedCheckCount++ - - got := "N/A" - if checkResult.Got != nil { - got = fmt.Sprintf("%t", *checkResult.Got) - } - - checkResultsOutput = fmt.Sprintf( - "%s\nⅹ Check(user=%s,relation=%s,object=%s): expected=%t, got=%s, error=%v", - checkResultsOutput, - checkResult.Request.User, - checkResult.Request.Relation, - checkResult.Request.Object, - checkResult.Expected, - got, - checkResult.Error, - ) - } - } - } - - if totalListObjectsCount > 0 { - for index := 0; index < totalListObjectsCount; index++ { - listObjectsResult := result.ListObjectsResults[index] - - if result.ListObjectsResults[index].IsPassing() { - listObjectsResultsOutput = fmt.Sprintf( - "%s\n✓ ListObjects(user=%s,relation=%s,type=%s)", - listObjectsResultsOutput, - listObjectsResult.Request.User, - listObjectsResult.Request.Relation, - listObjectsResult.Request.Type, - ) - } else { - failedListObjectsCount++ - - got := "N/A" - if listObjectsResult.Got != nil { - got = fmt.Sprintf("%s", *listObjectsResult.Got) - } - - listObjectsResultsOutput = fmt.Sprintf( - "%s\nⅹ ListObjects(user=%s,relation=%s,type=%s): expected=%s, got=%s, error=%v", - listObjectsResultsOutput, - listObjectsResult.Request.User, - listObjectsResult.Request.Relation, - listObjectsResult.Request.Type, - listObjectsResult.Expected, - got, - listObjectsResult.Error, - ) - } - } - } - - testStatus := "PASSING" - if failedCheckCount+failedListObjectsCount != 0 { - testStatus = "FAILING" - } - - output := fmt.Sprintf( - "(%s) %s: Checks (%d/%d passing) | ListObjects (%d/%d passing)", - testStatus, - result.Name, - totalCheckCount-failedCheckCount, - totalCheckCount, - totalListObjectsCount-failedListObjectsCount, - totalListObjectsCount, - ) - - if failedCheckCount > 0 { - output = fmt.Sprintf("%s%s", output, checkResultsOutput) - } - - if failedListObjectsCount > 0 { - output = fmt.Sprintf("%s%s", output, listObjectsResultsOutput) - } - - return output -} - -type ModelTestCheck struct { - User string `json:"user"` - Object string `json:"object"` - Assertions map[string]bool `json:"assertions"` -} - -type ModelTestListObjects struct { - User string `json:"user"` - Type string `json:"type"` - Assertions map[string][]string `json:"assertions"` -} - -type ModelTest struct { - Name string `json:"name"` - Description string `json:"description"` - Tuples []client.ClientTupleKey `json:"tuples"` - Check []ModelTestCheck `json:"check"` - ListObjects []ModelTestListObjects `json:"list_objects" yaml:"list-objects"` //nolint:tagliatelle -} - -func RunSingleCheckTest( - fgaClient *client.OpenFgaClient, - checkRequest client.ClientCheckRequest, - expectation bool, -) ModelTestCheckSingleResult { - response, err := fgaClient.Check(context.Background()). - Body(checkRequest). - Execute() - - result := ModelTestCheckSingleResult{ - Request: checkRequest, - Expected: expectation, - Error: err, - } - - if response != nil { - result.Got = response.Allowed - result.TestResult = result.IsPassing() - } - - return result -} - -func RunCheckTest( - fgaClient *client.OpenFgaClient, - checkTest ModelTestCheck, - tuples []client.ClientTupleKey, -) []ModelTestCheckSingleResult { - results := []ModelTestCheckSingleResult{} - - for relation, expectation := range checkTest.Assertions { - result := RunSingleCheckTest(fgaClient, - client.ClientCheckRequest{ - User: checkTest.User, - Relation: relation, - Object: checkTest.Object, - ContextualTuples: &tuples, - }, - expectation, - ) - results = append(results, result) - } - - return results -} - -func RunSingleListObjectsTest( - fgaClient *client.OpenFgaClient, - listObjectsRequest client.ClientListObjectsRequest, - expectation []string, -) ModelTestListObjectsSingleResult { - response, err := fgaClient.ListObjects(context.Background()). - Body(listObjectsRequest). - Execute() - - result := ModelTestListObjectsSingleResult{ - Request: listObjectsRequest, - Expected: expectation, - Error: err, - } - - if response != nil { - result.Got = response.Objects - result.TestResult = result.IsPassing() - } - - return result -} - -func RunListObjectsTest( - fgaClient *client.OpenFgaClient, - listObjectsTest ModelTestListObjects, - tuples []client.ClientTupleKey, -) []ModelTestListObjectsSingleResult { - results := []ModelTestListObjectsSingleResult{} - - for relation, expectation := range listObjectsTest.Assertions { - result := RunSingleListObjectsTest(fgaClient, - client.ClientListObjectsRequest{ - User: listObjectsTest.User, - Type: listObjectsTest.Type, - Relation: relation, - ContextualTuples: &tuples, - }, - expectation, - ) - results = append(results, result) - } - - return results -} - -func RunTest(fgaClient *client.OpenFgaClient, test ModelTest) TestResult { - checkResults := []ModelTestCheckSingleResult{} - - for index := 0; index < len(test.Check); index++ { - results := RunCheckTest(fgaClient, test.Check[index], test.Tuples) - checkResults = append(checkResults, results...) - } - - listObjectResults := []ModelTestListObjectsSingleResult{} - - for index := 0; index < len(test.ListObjects); index++ { - results := RunListObjectsTest(fgaClient, test.ListObjects[index], test.Tuples) - listObjectResults = append(listObjectResults, results...) - } - - return TestResult{ - Name: test.Name, - Description: test.Description, - CheckResults: checkResults, - ListObjectsResults: listObjectResults, - } -} - -func RunTests(fgaClient *client.OpenFgaClient, tests []ModelTest) []TestResult { - results := []TestResult{} - - for index := 0; index < len(tests); index++ { - result := RunTest(fgaClient, tests[index]) - results = append(results, result) - } - - return results -} diff --git a/internal/comparison/stringarraysequal.go b/internal/comparison/stringarraysequal.go new file mode 100644 index 0000000..ac8f834 --- /dev/null +++ b/internal/comparison/stringarraysequal.go @@ -0,0 +1,20 @@ +package comparison + +import "sort" + +func CheckStringArraysEqual(array1 []string, array2 []string) bool { + if len(array1) != len(array2) { + return false + } + + sort.Strings(array1) + sort.Strings(array2) + + for index, value := range array1 { + if value != array2[index] { + return false + } + } + + return true +} diff --git a/internal/storetest/localstore.go b/internal/storetest/localstore.go new file mode 100644 index 0000000..8b774f7 --- /dev/null +++ b/internal/storetest/localstore.go @@ -0,0 +1,106 @@ +package storetest + +import ( + "context" + "math" + + "github.com/oklog/ulid/v2" + pb "github.com/openfga/api/proto/openfga/v1" + "github.com/openfga/cli/internal/authorizationmodel" + "github.com/openfga/go-sdk/client" + "github.com/openfga/openfga/pkg/server" + "github.com/openfga/openfga/pkg/storage/memory" +) + +const writeMaxChunkSize = 40 + +func initLocalStore( + fgaServer *server.Server, + model *pb.AuthorizationModel, + testTuples []client.ClientTupleKey, +) (*string, *string, error) { + var modelID *string + + storeID := ulid.Make().String() + tuples := convertClientTupleKeysToProtoTupleKeys(testTuples) + + var authModelWriteReq *pb.WriteAuthorizationModelRequest + + if model != nil { + authModelWriteReq = &pb.WriteAuthorizationModelRequest{ + StoreId: storeID, + TypeDefinitions: model.GetTypeDefinitions(), + SchemaVersion: model.GetSchemaVersion(), + } + } + + if authModelWriteReq != nil { + writtenModel, err := fgaServer.WriteAuthorizationModel(context.Background(), authModelWriteReq) + if err != nil { + return nil, nil, err //nolint:wrapcheck + } + + modelID = &writtenModel.AuthorizationModelId + } + + tuplesLength := len(tuples) + if tuplesLength > 0 { + for i := 0; i < tuplesLength; i += writeMaxChunkSize { + end := int(math.Min(float64(i+writeMaxChunkSize), float64(tuplesLength))) + writeChunk := (tuples)[i:end] + + writeRequest := &pb.WriteRequest{ + StoreId: storeID, + Writes: &pb.TupleKeys{TupleKeys: writeChunk}, + } + + _, err := fgaServer.Write(context.Background(), writeRequest) + if err != nil { + return nil, nil, err //nolint:wrapcheck + } + } + } + + return &storeID, modelID, nil +} + +func getLocalServerAndModel( + storeData StoreData, + basePath string, +) (*server.Server, *authorizationmodel.AuthzModel, error) { + var fgaServer *server.Server + + var authModel *authorizationmodel.AuthzModel + + format, err := storeData.LoadModel(basePath) + if err != nil { + return nil, nil, err + } + + if storeData.Model == "" { + return fgaServer, authModel, nil + } + + // If we have at least one local test, initialize the local server + datastore := memory.New() + + fgaServer, err = server.NewServerWithOpts(server.WithDatastore(datastore)) + if err != nil { + return nil, nil, err //nolint:wrapcheck + } + + tempModel := authorizationmodel.AuthzModel{} + if format == authorizationmodel.ModelFormatJSON { + if err := tempModel.ReadFromJSONString(storeData.Model); err != nil { + return nil, nil, err //nolint:wrapcheck + } + } else { + if err := tempModel.ReadFromDSLString(storeData.Model); err != nil { + return nil, nil, err //nolint:wrapcheck + } + } + + authModel = &tempModel + + return fgaServer, authModel, nil +} diff --git a/internal/storetest/localtest.go b/internal/storetest/localtest.go new file mode 100644 index 0000000..dc15fe1 --- /dev/null +++ b/internal/storetest/localtest.go @@ -0,0 +1,160 @@ +package storetest + +import ( + "context" + + pb "github.com/openfga/api/proto/openfga/v1" + "github.com/openfga/cli/internal/authorizationmodel" + "github.com/openfga/go-sdk/client" + "github.com/openfga/openfga/pkg/server" +) + +func RunSingleLocalCheckTest( + fgaServer *server.Server, + checkRequest *pb.CheckRequest, + tuples []client.ClientTupleKey, + expectation bool, +) ModelTestCheckSingleResult { + result := ModelTestCheckSingleResult{ + Request: client.ClientCheckRequest{ + User: checkRequest.GetTupleKey().GetUser(), + Relation: checkRequest.GetTupleKey().GetRelation(), + Object: checkRequest.GetTupleKey().GetObject(), + ContextualTuples: &tuples, + }, + Expected: expectation, + } + + res, err := fgaServer.Check(context.Background(), checkRequest) + if err != nil { + result.Error = err + + return result + } + + if res != nil { + result.Got = &res.Allowed + result.TestResult = result.IsPassing() + } + + return result +} + +func RunLocalCheckTest( + fgaServer *server.Server, + checkTest ModelTestCheck, + tuples []client.ClientTupleKey, + options ModelTestOptions, +) []ModelTestCheckSingleResult { + results := []ModelTestCheckSingleResult{} + + for relation, expectation := range checkTest.Assertions { + result := RunSingleLocalCheckTest( + fgaServer, + &pb.CheckRequest{ + StoreId: *options.StoreID, + AuthorizationModelId: *options.ModelID, + TupleKey: &pb.TupleKey{ + User: checkTest.User, + Relation: relation, + Object: checkTest.Object, + }, + }, + tuples, + expectation, + ) + results = append(results, result) + } + + return results +} + +func RunSingleLocalListObjectsTest( + fgaServer *server.Server, + listObjectsRequest *pb.ListObjectsRequest, + tuples []client.ClientTupleKey, + expectation []string, +) ModelTestListObjectsSingleResult { + response, err := fgaServer.ListObjects(context.Background(), listObjectsRequest) + + result := ModelTestListObjectsSingleResult{ + Request: client.ClientListObjectsRequest{ + User: listObjectsRequest.GetUser(), + Relation: listObjectsRequest.GetRelation(), + Type: listObjectsRequest.GetType(), + ContextualTuples: &tuples, + }, + Expected: expectation, + Error: err, + } + + if response != nil { + result.Got = &response.Objects + result.TestResult = result.IsPassing() + } + + return result +} + +func RunLocalListObjectsTest( + fgaServer *server.Server, + listObjectsTest ModelTestListObjects, + tuples []client.ClientTupleKey, + options ModelTestOptions, +) []ModelTestListObjectsSingleResult { + results := []ModelTestListObjectsSingleResult{} + + for relation, expectation := range listObjectsTest.Assertions { + result := RunSingleLocalListObjectsTest(fgaServer, + &pb.ListObjectsRequest{ + StoreId: *options.StoreID, + AuthorizationModelId: *options.ModelID, + User: listObjectsTest.User, + Type: listObjectsTest.Type, + Relation: relation, + }, + tuples, + expectation, + ) + results = append(results, result) + } + + return results +} + +func RunLocalTest( + fgaServer *server.Server, + test ModelTest, + tuples []client.ClientTupleKey, + model *authorizationmodel.AuthzModel, +) (TestResult, error) { + checkResults := []ModelTestCheckSingleResult{} + listObjectResults := []ModelTestListObjectsSingleResult{} + + storeID, modelID, err := initLocalStore(fgaServer, model.GetProtoModel(), tuples) + if err != nil { + return TestResult{}, err + } + + testOptions := ModelTestOptions{ + StoreID: storeID, + ModelID: modelID, + } + + for index := 0; index < len(test.Check); index++ { + results := RunLocalCheckTest(fgaServer, test.Check[index], tuples, testOptions) + checkResults = append(checkResults, results...) + } + + for index := 0; index < len(test.ListObjects); index++ { + results := RunLocalListObjectsTest(fgaServer, test.ListObjects[index], tuples, testOptions) + listObjectResults = append(listObjectResults, results...) + } + + return TestResult{ + Name: test.Name, + Description: test.Description, + CheckResults: checkResults, + ListObjectsResults: listObjectResults, + }, nil +} diff --git a/internal/storetest/remotetest.go b/internal/storetest/remotetest.go new file mode 100644 index 0000000..e5105e0 --- /dev/null +++ b/internal/storetest/remotetest.go @@ -0,0 +1,119 @@ +package storetest + +import ( + "context" + + "github.com/openfga/go-sdk/client" +) + +func RunSingleRemoteCheckTest( + fgaClient *client.OpenFgaClient, + checkRequest client.ClientCheckRequest, + expectation bool, +) ModelTestCheckSingleResult { + res, err := fgaClient.Check(context.Background()).Body(checkRequest).Execute() + + result := ModelTestCheckSingleResult{ + Request: checkRequest, + Expected: expectation, + Error: err, + } + + if err == nil && res != nil { + result.Got = res.Allowed + result.TestResult = result.IsPassing() + } + + return result +} + +func RunRemoteCheckTest( + fgaClient *client.OpenFgaClient, + checkTest ModelTestCheck, + tuples []client.ClientTupleKey, +) []ModelTestCheckSingleResult { + results := []ModelTestCheckSingleResult{} + + for relation, expectation := range checkTest.Assertions { + result := RunSingleRemoteCheckTest( + fgaClient, + client.ClientCheckRequest{ + User: checkTest.User, + Relation: relation, + Object: checkTest.Object, + ContextualTuples: &tuples, + }, + expectation, + ) + results = append(results, result) + } + + return results +} + +func RunSingleRemoteListObjectsTest( + fgaClient *client.OpenFgaClient, + listObjectsRequest client.ClientListObjectsRequest, + expectation []string, +) ModelTestListObjectsSingleResult { + response, err := fgaClient.ListObjects(context.Background()).Body(listObjectsRequest).Execute() + + result := ModelTestListObjectsSingleResult{ + Request: listObjectsRequest, + Expected: expectation, + Error: err, + } + + if response != nil { + result.Got = response.Objects + result.TestResult = result.IsPassing() + } + + return result +} + +func RunRemoteListObjectsTest( + fgaClient *client.OpenFgaClient, + listObjectsTest ModelTestListObjects, + tuples []client.ClientTupleKey, +) []ModelTestListObjectsSingleResult { + results := []ModelTestListObjectsSingleResult{} + + for relation, expectation := range listObjectsTest.Assertions { + result := RunSingleRemoteListObjectsTest(fgaClient, + client.ClientListObjectsRequest{ + User: listObjectsTest.User, + Type: listObjectsTest.Type, + Relation: relation, + ContextualTuples: &tuples, + }, + expectation, + ) + results = append(results, result) + } + + return results +} + +func RunRemoteTest(fgaClient *client.OpenFgaClient, test ModelTest, testTuples []client.ClientTupleKey) TestResult { + checkResults := []ModelTestCheckSingleResult{} + + for index := 0; index < len(test.Check); index++ { + results := RunRemoteCheckTest(fgaClient, test.Check[index], testTuples) + checkResults = append(checkResults, results...) + } + + listObjectResults := []ModelTestListObjectsSingleResult{} + + for index := 0; index < len(test.ListObjects); index++ { + results := RunRemoteListObjectsTest(fgaClient, test.ListObjects[index], testTuples) + listObjectResults = append(listObjectResults, results...) + } + + return TestResult{ + Name: test.Name, + Description: test.Description, + CheckResults: checkResults, + ListObjectsResults: listObjectResults, + } +} diff --git a/internal/storetest/storedata.go b/internal/storetest/storedata.go new file mode 100644 index 0000000..0b6903e --- /dev/null +++ b/internal/storetest/storedata.go @@ -0,0 +1,81 @@ +/* +Copyright © 2023 OpenFGA + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package storetest contains cli specific store interfaces and functionality +package storetest + +import ( + "path" + + "github.com/openfga/cli/internal/authorizationmodel" + "github.com/openfga/go-sdk/client" +) + +type ModelTestCheck struct { + User string `json:"user" yaml:"user"` + Object string `json:"object" yaml:"object"` + Assertions map[string]bool `json:"assertions" yaml:"assertions"` +} + +type ModelTestListObjects struct { + User string `json:"user" yaml:"user"` + Type string `json:"type" yaml:"type"` + Assertions map[string][]string `json:"assertions" yaml:"assertions"` +} + +type ModelTest struct { + Name string `json:"name" yaml:"name"` + Description string `json:"description" yaml:"description"` + Tuples []client.ClientTupleKey `json:"tuples" yaml:"tuples"` + Check []ModelTestCheck `json:"check" yaml:"check"` + ListObjects []ModelTestListObjects `json:"list_objects" yaml:"list_objects"` //nolint:tagliatelle +} + +type StoreData struct { + Name string `json:"name" yaml:"name"` + Model string `json:"model" yaml:"model"` + ModelFile string `json:"model_file" yaml:"model_file"` //nolint:tagliatelle + Tuples []client.ClientTupleKey `json:"tuples" yaml:"tuples"` + Tests []ModelTest `json:"tests" yaml:"tests"` +} + +func (storeData *StoreData) LoadModel(basePath string) (authorizationmodel.ModelFormat, error) { + format := authorizationmodel.ModelFormatDefault + if storeData.Model != "" { + return format, nil + } + + if storeData.ModelFile == "" { + return format, nil + } + + var inputModel string + + storeName := storeData.Name + if err := authorizationmodel.ReadFromFile( + path.Join(basePath, storeData.ModelFile), + &inputModel, + &format, + &storeName); err != nil { + return format, err //nolint:wrapcheck + } + + if inputModel != "" { + storeData.Model = inputModel + } + + return format, nil +} diff --git a/internal/storetest/testresult.go b/internal/storetest/testresult.go new file mode 100644 index 0000000..080bca6 --- /dev/null +++ b/internal/storetest/testresult.go @@ -0,0 +1,142 @@ +package storetest + +import ( + "fmt" + + "github.com/openfga/cli/internal/comparison" + "github.com/openfga/go-sdk/client" +) + +type ModelTestCheckSingleResult struct { + Request client.ClientCheckRequest `json:"request"` + Expected bool `json:"expected"` + Got *bool `json:"got"` + Error error `json:"error"` + TestResult bool `json:"test_result"` +} + +func (result ModelTestCheckSingleResult) IsPassing() bool { + return result.Error == nil && *result.Got == result.Expected +} + +type ModelTestListObjectsSingleResult struct { + Request client.ClientListObjectsRequest `json:"request"` + Expected []string `json:"expected"` + Got *[]string `json:"got"` + Error error `json:"error"` + TestResult bool `json:"test_result"` +} + +func (result ModelTestListObjectsSingleResult) IsPassing() bool { + return result.Error == nil && result.Got != nil && comparison.CheckStringArraysEqual(*result.Got, result.Expected) +} + +type TestResult struct { + Name string `json:"name"` + Description string `json:"description"` + CheckResults []ModelTestCheckSingleResult `json:"check_results"` + ListObjectsResults []ModelTestListObjectsSingleResult `json:"list_objects_results"` +} + +//nolint:cyclop +func (result TestResult) FriendlyDisplay() string { + totalCheckCount := len(result.CheckResults) + failedCheckCount := 0 + totalListObjectsCount := len(result.ListObjectsResults) + failedListObjectsCount := 0 + checkResultsOutput := "" + listObjectsResultsOutput := "" + + if totalCheckCount > 0 { + for index := 0; index < totalCheckCount; index++ { + checkResult := result.CheckResults[index] + + if result.CheckResults[index].IsPassing() { + checkResultsOutput = fmt.Sprintf( + "%s\n✓ Check(user=%s,relation=%s,object=%s)", + checkResultsOutput, + checkResult.Request.User, + checkResult.Request.Relation, + checkResult.Request.Object, + ) + } else { + failedCheckCount++ + + got := "N/A" + if checkResult.Got != nil { + got = fmt.Sprintf("%t", *checkResult.Got) + } + + checkResultsOutput = fmt.Sprintf( + "%s\nⅹ Check(user=%s,relation=%s,object=%s): expected=%t, got=%s, error=%v", + checkResultsOutput, + checkResult.Request.User, + checkResult.Request.Relation, + checkResult.Request.Object, + checkResult.Expected, + got, + checkResult.Error, + ) + } + } + } + + if totalListObjectsCount > 0 { + for index := 0; index < totalListObjectsCount; index++ { + listObjectsResult := result.ListObjectsResults[index] + + if result.ListObjectsResults[index].IsPassing() { + listObjectsResultsOutput = fmt.Sprintf( + "%s\n✓ ListObjects(user=%s,relation=%s,type=%s)", + listObjectsResultsOutput, + listObjectsResult.Request.User, + listObjectsResult.Request.Relation, + listObjectsResult.Request.Type, + ) + } else { + failedListObjectsCount++ + + got := "N/A" + if listObjectsResult.Got != nil { + got = fmt.Sprintf("%s", *listObjectsResult.Got) + } + + listObjectsResultsOutput = fmt.Sprintf( + "%s\nⅹ ListObjects(user=%s,relation=%s,type=%s): expected=%s, got=%s, error=%v", + listObjectsResultsOutput, + listObjectsResult.Request.User, + listObjectsResult.Request.Relation, + listObjectsResult.Request.Type, + listObjectsResult.Expected, + got, + listObjectsResult.Error, + ) + } + } + } + + testStatus := "PASSING" + if failedCheckCount+failedListObjectsCount != 0 { + testStatus = "FAILING" + } + + output := fmt.Sprintf( + "(%s) %s: Checks (%d/%d passing) | ListObjects (%d/%d passing)", + testStatus, + result.Name, + totalCheckCount-failedCheckCount, + totalCheckCount, + totalListObjectsCount-failedListObjectsCount, + totalListObjectsCount, + ) + + if failedCheckCount > 0 { + output = fmt.Sprintf("%s%s", output, checkResultsOutput) + } + + if failedListObjectsCount > 0 { + output = fmt.Sprintf("%s%s", output, listObjectsResultsOutput) + } + + return output +} diff --git a/internal/storetest/tests.go b/internal/storetest/tests.go new file mode 100644 index 0000000..1c0408b --- /dev/null +++ b/internal/storetest/tests.go @@ -0,0 +1,59 @@ +package storetest + +import ( + "github.com/openfga/cli/internal/authorizationmodel" + "github.com/openfga/go-sdk/client" + "github.com/openfga/openfga/pkg/server" +) + +type ModelTestOptions struct { + StoreID *string + ModelID *string + Remote bool +} + +func RunTest( + fgaClient *client.OpenFgaClient, + fgaServer *server.Server, + test ModelTest, + globalTuples []client.ClientTupleKey, + model *authorizationmodel.AuthzModel, +) (TestResult, error) { + testTuples := append(append([]client.ClientTupleKey{}, globalTuples...), test.Tuples...) + + if model == nil { + return RunRemoteTest(fgaClient, test, testTuples), nil + } + + return RunLocalTest(fgaServer, test, testTuples, model) +} + +func RunTests( + fgaClient *client.OpenFgaClient, + storeData StoreData, + basePath string, +) ([]TestResult, error) { + results := []TestResult{} + + fgaServer, authModel, err := getLocalServerAndModel(storeData, basePath) + if err != nil { + return results, err + } + + for index := 0; index < len(storeData.Tests); index++ { + result, err := RunTest( + fgaClient, + fgaServer, + storeData.Tests[index], + storeData.Tuples, + authModel, + ) + if err != nil { + return results, err + } + + results = append(results, result) + } + + return results, nil +} diff --git a/internal/storetest/tuplekey.go b/internal/storetest/tuplekey.go new file mode 100644 index 0000000..4446b90 --- /dev/null +++ b/internal/storetest/tuplekey.go @@ -0,0 +1,22 @@ +package storetest + +import ( + pb "github.com/openfga/api/proto/openfga/v1" + "github.com/openfga/go-sdk/client" +) + +func convertClientTupleKeysToProtoTupleKeys(tuples []client.ClientTupleKey) []*pb.TupleKey { + pbTuples := []*pb.TupleKey{} + + for index := 0; index < len(tuples); index++ { + tuple := tuples[index] + tpl := pb.TupleKey{ + User: tuple.User, + Relation: tuple.Relation, + Object: tuple.Object, + } + pbTuples = append(pbTuples, &tpl) + } + + return pbTuples +}