From 5a1b0bacfe9f1e487c334c9cf52bc3c729a73dce Mon Sep 17 00:00:00 2001 From: Patrik Date: Fri, 24 Sep 2021 12:50:33 +0200 Subject: [PATCH] refactor: make subject sets and subject IDs unambiguous (#729) BREAKING CHANGES: This patch changes the payload of the REST API. The gRPC API is **not** affected. The parameter `subject` was previously an encoded string. With this change clients have to explicitly use either `subject_id` or (`subject_set.namespace` and `subject_set.object` and `subject_set.relation`). The same is true for REST responses returned by Keto. An error with a hint will be returned if `subject` is still used. --- .circleci/config.yml | 16 +- .github/workflows/buf.yml | 2 +- .github/workflows/release-go-grpc-client.yml | 2 +- .schema/relation_tuple.schema.json | 92 +- cmd/relationtuple/get.go | 42 +- .../relation-tuples/cats1_owner.json | 6 +- .../relation-tuples/cats1_view_owner.json | 6 +- .../relation-tuples/cats1_view_public.json | 2 +- .../relation-tuples/cats2_owner.json | 6 +- .../relation-tuples/cats2_view.json | 6 +- .../relation-tuples/cats_owner.json | 2 +- .../relation-tuples/cats_view.json | 6 +- .../01-expand-beach/expected_output.txt | 34 +- .../01-expand-beach/index.js | 18 +- .../01-list-PM/cli.sh | 2 +- .../01-list-PM/curl.sh | 2 +- .../02-list-coffee-break/cli.sh | 2 +- .../02-list-coffee-break/curl.sh | 2 +- .../00-write-direct-access/curl.sh | 2 +- .../01-check-direct-access/curl.sh | 2 +- .../99-cleanup/cli.sh | 2 +- .../99-cleanup/curl.sh | 2 +- docs/docs/cli/keto-relation-tuple-get.md | 3 +- go.mod | 153 ++- internal/check/handler.go | 10 +- internal/check/handler_test.go | 20 +- internal/e2e/cases_test.go | 13 +- internal/e2e/cli_client_test.go | 7 +- internal/e2e/grpc_client_test.go | 8 +- internal/e2e/rest_client_test.go | 8 +- ...http_client_test.go => sdk_client_test.go} | 107 +- internal/expand/handler.go | 3 +- internal/expand/tree.go | 86 +- .../client/read/get_check_parameters.go | 136 +- .../client/read/get_expand_parameters.go | 6 +- .../read/get_relation_tuples_parameters.go | 192 ++- .../client/read/post_check_parameters.go | 6 +- .../write/create_relation_tuple_parameters.go | 6 +- .../write/create_relation_tuple_responses.go | 8 +- .../write/delete_relation_tuple_parameters.go | 136 +- internal/httpclient/models/expand_tree.go | 37 +- .../models/internal_relation_tuple.go | 45 +- internal/httpclient/models/relation_query.go | 129 ++ internal/httpclient/models/subject.go | 27 - internal/httpclient/models/subject_set.go | 105 ++ internal/persistence/sql/relationtuples.go | 4 +- internal/relationtuple/definitions.go | 290 ++-- internal/relationtuple/definitions_test.go | 124 +- .../relationtuple/manager_requirements.go | 16 +- internal/relationtuple/read_server.go | 15 +- internal/relationtuple/swagger_definitions.go | 43 + internal/relationtuple/transact_server.go | 60 +- .../relationtuple/transact_server_test.go | 24 +- spec/api.json | 165 ++- spec/swagger.json | 1215 +++++++++++++++++ 55 files changed, 2944 insertions(+), 519 deletions(-) rename internal/e2e/{http_client_test.go => sdk_client_test.go} (67%) create mode 100644 internal/httpclient/models/relation_query.go delete mode 100644 internal/httpclient/models/subject.go create mode 100644 internal/httpclient/models/subject_set.go create mode 100644 internal/relationtuple/swagger_definitions.go create mode 100755 spec/swagger.json diff --git a/.circleci/config.yml b/.circleci/config.yml index 9658b440a..15611e4cb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,7 +14,7 @@ orbs: jobs: test: docker: - - image: cimg/go:1.16 + - image: cimg/go:1.17 environment: TEST_DATABASE_POSTGRESQL: postgres://test:test@localhost:5432/keto?sslmode=disable TEST_DATABASE_MYSQL: mysql://root:test@(localhost:3306)/mysql?parseTime=true&multiStatements=true @@ -53,7 +53,7 @@ jobs: test-race: docker: - - image: cimg/go:1.16 + - image: cimg/go:1.17 steps: - checkout - go/load-cache @@ -64,7 +64,7 @@ jobs: validate: docker: - - image: cimg/go:1.16-node + - image: cimg/go:1.17-node steps: - checkout @@ -95,6 +95,14 @@ jobs: # Test documentation examples - run: make test-docs-samples + clidocs: + docker: + - image: cimg/go:1.17-node + steps: + - checkout + - run: + name: Build and push CLI docs + command: "bash <(curl -s https://raw.githubusercontent.com/ory/ci/master/src/scripts/docs/cli.sh)" workflows: version: 2 @@ -124,7 +132,7 @@ workflows: only: /v.*/ branches: only: master - - docs/cli + - clidocs - docs/build: requires: - test diff --git a/.github/workflows/buf.yml b/.github/workflows/buf.yml index 217bfab47..60b87533c 100644 --- a/.github/workflows/buf.yml +++ b/.github/workflows/buf.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: - go-version: '1.16' + go-version: '1.17' - uses: actions/setup-node@v2 with: node-version: '15' diff --git a/.github/workflows/release-go-grpc-client.yml b/.github/workflows/release-go-grpc-client.yml index 3df361a23..e6064f60e 100644 --- a/.github/workflows/release-go-grpc-client.yml +++ b/.github/workflows/release-go-grpc-client.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: - go-version: '1.16' + go-version: '1.17' - name: Download dependencies run: cd proto; go mod tidy - name: Test diff --git a/.schema/relation_tuple.schema.json b/.schema/relation_tuple.schema.json index 203b4108d..80d796d04 100644 --- a/.schema/relation_tuple.schema.json +++ b/.schema/relation_tuple.schema.json @@ -2,40 +2,78 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "additionalProperties": false, - "required": ["namespace", "relation", "object", "subject"], - "properties": { - "$schema": { - "type": "string", - "format": "uri-reference", - "description": "Add this to allow defining the schema, useful for IDE integration" - }, - "namespace": { - "type": "string", - "description": "The namespace of the object and relation in this tuple." - }, - "relation": { - "type": "string", - "description": "The relation of the object and subject." - }, - "object": { - "type": "string", - "description": "The object affected by this relation." + "allOf": [ + { + "required": [ + "namespace", + "relation", + "object" + ], + "properties": { + "$schema": { + "type": "string", + "format": "uri-reference", + "description": "Add this to allow defining the schema, useful for IDE integration" + }, + "namespace": { + "type": "string", + "description": "The namespace of the object and relation in this tuple." + }, + "object": { + "type": "string", + "description": "The object affected by this relation." + }, + "relation": { + "type": "string", + "description": "The relation of the object and subject." + } + } }, - "subject": { + { "oneOf": [ { - "type": "string", - "pattern": "^.*:.*#.*$", - "description": "The subject set affected by this relation. Uses the encoding of \":#\"." + "required": [ + "subject_id" + ], + "properties": { + "subject_id": { + "type": "string", + "description": "The subject ID affected by this relation." + } + } }, { - "type": "string", - "description": "The subject affected by this relation. Use \":#\" to describe a subject set.", - "not": { - "pattern": "^.*:.*#.*$" + "required": [ + "subject_set" + ], + "properties": { + "subject_set": { + "type": "object", + "description": "The subject set affected by this relation.", + "properties": { + "namespace": { + "type": "string", + "description": "The namespace of the object and relation in this subject set." + }, + "object": { + "type": "string", + "description": "The object referenced in this subject set." + }, + "relation": { + "type": "string", + "description": "The relation of this subject set." + } + }, + "additionalProperties": false, + "required": [ + "namespace", + "relation", + "object" + ] + } } } ] } - } + ] } diff --git a/cmd/relationtuple/get.go b/cmd/relationtuple/get.go index 2a2932149..0ac23b534 100644 --- a/cmd/relationtuple/get.go +++ b/cmd/relationtuple/get.go @@ -20,36 +20,44 @@ import ( ) const ( - FlagSubject = "subject" - FlagRelation = "relation" - FlagObject = "object" - FlagPageSize = "page-size" - FlagPageToken = "page-token" + FlagSubject = "subject" + FlagSubjectID = "subject-id" + FlagSubjectSet = "subject-set" + FlagRelation = "relation" + FlagObject = "object" + FlagPageSize = "page-size" + FlagPageToken = "page-token" ) func registerRelationTupleFlags(flags *pflag.FlagSet) { - flags.String(FlagSubject, "", "Set the requested subject") + flags.String(FlagSubjectID, "", "Set the requested subject ID") + flags.String(FlagSubjectSet, "", `Set the requested subject set; format: "namespace:object#relation"`) flags.String(FlagRelation, "", "Set the requested relation") flags.String(FlagObject, "", "Set the requested object") + + flags.String(FlagSubject, "", "") + if err := flags.MarkHidden(FlagSubject); err != nil { + panic(err.Error()) + } } func readQueryFromFlags(cmd *cobra.Command, namespace string) (*acl.ListRelationTuplesRequest_Query, error) { - subject := flagx.MustGetString(cmd, FlagSubject) - relation := flagx.MustGetString(cmd, FlagRelation) - object := flagx.MustGetString(cmd, FlagObject) - query := &acl.ListRelationTuplesRequest_Query{ - Relation: relation, - Object: object, Namespace: namespace, + Object: flagx.MustGetString(cmd, FlagObject), + Relation: flagx.MustGetString(cmd, FlagRelation), } - if subject != "" { - s, err := relationtuple.SubjectFromString(subject) + switch flags := cmd.Flags(); { + case flags.Changed(FlagSubjectID) && flags.Changed(FlagSubjectSet): + return nil, relationtuple.ErrDuplicateSubject + case flags.Changed(FlagSubjectID): + query.Subject = (&relationtuple.SubjectID{ID: flagx.MustGetString(cmd, FlagSubjectID)}).ToProto() + case flags.Changed(FlagSubjectSet): + s, err := (&relationtuple.SubjectSet{}).FromString(flagx.MustGetString(cmd, FlagSubjectSet)) if err != nil { return nil, err } - query.Subject = s.ToProto() } @@ -69,6 +77,10 @@ func newGetCmd() *cobra.Command { "Returns paginated results.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + if cmd.Flags().Changed(FlagSubject) { + return fmt.Errorf("usage of --%s is not supported anymore, use --%s or --%s respectively", FlagSubject, FlagSubjectID, FlagSubjectSet) + } + conn, err := client.GetReadConn(cmd) if err != nil { return err diff --git a/contrib/cat-videos-example/relation-tuples/cats1_owner.json b/contrib/cat-videos-example/relation-tuples/cats1_owner.json index 042d3191a..515bc1665 100644 --- a/contrib/cat-videos-example/relation-tuples/cats1_owner.json +++ b/contrib/cat-videos-example/relation-tuples/cats1_owner.json @@ -3,5 +3,9 @@ "namespace": "videos", "object": "/cats/1.mp4", "relation": "owner", - "subject": "videos:/cats#owner" + "subject_set": { + "namespace": "videos", + "object": "/cats", + "relation": "owner" + } } diff --git a/contrib/cat-videos-example/relation-tuples/cats1_view_owner.json b/contrib/cat-videos-example/relation-tuples/cats1_view_owner.json index eb8c604d3..d48d624b3 100644 --- a/contrib/cat-videos-example/relation-tuples/cats1_view_owner.json +++ b/contrib/cat-videos-example/relation-tuples/cats1_view_owner.json @@ -3,5 +3,9 @@ "namespace": "videos", "object": "/cats/1.mp4", "relation": "view", - "subject": "videos:/cats/1.mp4#owner" + "subject_set": { + "namespace": "videos", + "object": "/cats/1.mp4", + "relation": "owner" + } } diff --git a/contrib/cat-videos-example/relation-tuples/cats1_view_public.json b/contrib/cat-videos-example/relation-tuples/cats1_view_public.json index 008ab1ad3..dfb2b1ee4 100644 --- a/contrib/cat-videos-example/relation-tuples/cats1_view_public.json +++ b/contrib/cat-videos-example/relation-tuples/cats1_view_public.json @@ -3,5 +3,5 @@ "namespace": "videos", "object": "/cats/1.mp4", "relation": "view", - "subject": "*" + "subject_id": "*" } diff --git a/contrib/cat-videos-example/relation-tuples/cats2_owner.json b/contrib/cat-videos-example/relation-tuples/cats2_owner.json index a3ad91ec6..3702371e9 100644 --- a/contrib/cat-videos-example/relation-tuples/cats2_owner.json +++ b/contrib/cat-videos-example/relation-tuples/cats2_owner.json @@ -3,5 +3,9 @@ "namespace": "videos", "object": "/cats/2.mp4", "relation": "owner", - "subject": "videos:/cats#owner" + "subject_set": { + "namespace": "videos", + "object": "/cats", + "relation": "owner" + } } diff --git a/contrib/cat-videos-example/relation-tuples/cats2_view.json b/contrib/cat-videos-example/relation-tuples/cats2_view.json index 8531f904e..e72f20e1e 100644 --- a/contrib/cat-videos-example/relation-tuples/cats2_view.json +++ b/contrib/cat-videos-example/relation-tuples/cats2_view.json @@ -3,5 +3,9 @@ "namespace": "videos", "object": "/cats/2.mp4", "relation": "view", - "subject": "videos:/cats/2.mp4#owner" + "subject_set": { + "namespace": "videos", + "object": "/cats/2.mp4", + "relation": "owner" + } } diff --git a/contrib/cat-videos-example/relation-tuples/cats_owner.json b/contrib/cat-videos-example/relation-tuples/cats_owner.json index 5859a71d5..4486fb822 100644 --- a/contrib/cat-videos-example/relation-tuples/cats_owner.json +++ b/contrib/cat-videos-example/relation-tuples/cats_owner.json @@ -3,5 +3,5 @@ "namespace": "videos", "object": "/cats", "relation": "owner", - "subject": "cat lady" + "subject_id": "cat lady" } diff --git a/contrib/cat-videos-example/relation-tuples/cats_view.json b/contrib/cat-videos-example/relation-tuples/cats_view.json index d07257e90..6dfbd07c4 100644 --- a/contrib/cat-videos-example/relation-tuples/cats_view.json +++ b/contrib/cat-videos-example/relation-tuples/cats_view.json @@ -3,5 +3,9 @@ "namespace": "videos", "object": "/cats", "relation": "view", - "subject": "videos:/cats#owner" + "subject_set": { + "namespace": "videos", + "object": "/cats", + "relation": "owner" + } } diff --git a/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/expected_output.txt b/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/expected_output.txt index ac48d83b3..c0c2556b5 100644 --- a/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/expected_output.txt +++ b/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/expected_output.txt @@ -1,30 +1,46 @@ { "type": "union", - "subject": "files:/photos/beach.jpg#access", "children": [ { "type": "union", - "subject": "files:/photos/beach.jpg#owner", "children": [ { "type": "leaf", - "subject": "maureen" + "subject_id": "maureen" } - ] + ], + "subject_set": { + "namespace": "files", + "object": "/photos/beach.jpg", + "relation": "owner" + } }, { "type": "union", - "subject": "directories:/photos#access", "children": [ { "type": "leaf", - "subject": "directories:/photos#owner" + "subject_set": { + "namespace": "directories", + "object": "/photos", + "relation": "owner" + } }, { "type": "leaf", - "subject": "laura" + "subject_id": "laura" } - ] + ], + "subject_set": { + "namespace": "directories", + "object": "/photos", + "relation": "access" + } } - ] + ], + "subject_set": { + "namespace": "files", + "object": "/photos/beach.jpg", + "relation": "access" + } } diff --git a/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/index.js b/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/index.js index 2f8cf6ca1..a8b15b2cf 100644 --- a/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/index.js +++ b/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/index.js @@ -19,26 +19,32 @@ expandRequest.setSubject(sub) expandRequest.setMaxDepth(3) // helper to get a nice result -const subjectString = (subject) => { +const subjectJSON = (subject) => { if (subject.hasId()) { - return subject.getId() + return { subject_id: subject.getId() } } const set = subject.getSet() - return set.getNamespace() + ':' + set.getObject() + '#' + set.getRelation() + return { + subject_set: { + namespace: set.getNamespace(), + object: set.getObject(), + relation: set.getRelation() + } + } } // helper to get a nice result const prettyTree = (tree) => { const [nodeType, subject, children] = [ tree.getNodeType(), - subjectString(tree.getSubject()), + subjectJSON(tree.getSubject()), tree.getChildrenList() ] switch (nodeType) { case expand.NodeType.NODE_TYPE_LEAF: - return { type: 'leaf', subject } + return { type: 'leaf', ...subject } case expand.NodeType.NODE_TYPE_UNION: - return { type: 'union', subject, children: children.map(prettyTree) } + return { type: 'union', children: children.map(prettyTree), ...subject } } } diff --git a/contrib/docs-code-samples/list-api-display-objects/01-list-PM/cli.sh b/contrib/docs-code-samples/list-api-display-objects/01-list-PM/cli.sh index a0763cc68..0d593e79a 100755 --- a/contrib/docs-code-samples/list-api-display-objects/01-list-PM/cli.sh +++ b/contrib/docs-code-samples/list-api-display-objects/01-list-PM/cli.sh @@ -3,5 +3,5 @@ set -euo pipefail export KETO_READ_REMOTE="127.0.0.1:4466" -keto relation-tuple get chats --relation member --subject PM --format json | \ +keto relation-tuple get chats --relation member --subject-id PM --format json | \ jq ".relation_tuples[] | .object" -r diff --git a/contrib/docs-code-samples/list-api-display-objects/01-list-PM/curl.sh b/contrib/docs-code-samples/list-api-display-objects/01-list-PM/curl.sh index 60b566cb0..d936d8e0a 100755 --- a/contrib/docs-code-samples/list-api-display-objects/01-list-PM/curl.sh +++ b/contrib/docs-code-samples/list-api-display-objects/01-list-PM/curl.sh @@ -4,6 +4,6 @@ set -euo pipefail curl -G --silent \ --data-urlencode "namespace=chats" \ --data-urlencode "relation=member" \ - --data-urlencode "subject=PM" \ + --data-urlencode "subject_id=PM" \ http://127.0.0.1:4466/relation-tuples | \ jq ".relation_tuples[] | .object" -r diff --git a/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/cli.sh b/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/cli.sh index d189f8414..7b73f81f8 100755 --- a/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/cli.sh +++ b/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/cli.sh @@ -4,4 +4,4 @@ set -euo pipefail export KETO_READ_REMOTE="127.0.0.1:4466" keto relation-tuple get chats --object coffee-break --relation member --format json | \ - jq ".relation_tuples[] | .subject" -r + jq ".relation_tuples[] | .subject_id" -r diff --git a/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/curl.sh b/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/curl.sh index aa716aa05..5439bab62 100755 --- a/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/curl.sh +++ b/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/curl.sh @@ -6,4 +6,4 @@ curl -G --silent \ --data-urlencode "object=coffee-break" \ --data-urlencode "relation=member" \ http://127.0.0.1:4466/relation-tuples | \ - jq ".relation_tuples[] | .subject" -r + jq ".relation_tuples[] | .subject_id" -r diff --git a/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/curl.sh b/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/curl.sh index c7cfffe2a..8e5f8af1f 100755 --- a/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/curl.sh +++ b/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/curl.sh @@ -6,7 +6,7 @@ relationtuple=' "namespace": "messages", "object": "02y_15_4w350m3", "relation": "decypher", - "subject": "john" + "subject_id": "john" }' curl --fail --silent -X PUT \ diff --git a/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/curl.sh b/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/curl.sh index 89af3d84c..45f283d9c 100755 --- a/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/curl.sh +++ b/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/curl.sh @@ -2,7 +2,7 @@ set -euo pipefail curl -G --silent \ - --data-urlencode "subject=john" \ + --data-urlencode "subject_id=john" \ --data-urlencode "relation=decypher" \ --data-urlencode "namespace=messages" \ --data-urlencode "object=02y_15_4w350m3" \ diff --git a/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/cli.sh b/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/cli.sh index ba2856af2..1b05f69d8 100755 --- a/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/cli.sh +++ b/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/cli.sh @@ -8,7 +8,7 @@ relationtuple=' "namespace": "messages", "object": "02y_15_4w350m3", "relation": "decypher", - "subject": "john" + "subject_id": "john" }' keto relation-tuple delete <(echo "$relationtuple") -q > /dev/null diff --git a/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/curl.sh b/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/curl.sh index f0e4099d0..719f5bee9 100755 --- a/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/curl.sh +++ b/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/curl.sh @@ -2,7 +2,7 @@ set -euo pipefail curl -X DELETE -G --silent \ - --data-urlencode "subject=john" \ + --data-urlencode "subject_id=john" \ --data-urlencode "relation=decypher" \ --data-urlencode "namespace=messages" \ --data-urlencode "object=02y_15_4w350m3" \ diff --git a/docs/docs/cli/keto-relation-tuple-get.md b/docs/docs/cli/keto-relation-tuple-get.md index bd031bec3..f09926ecb 100644 --- a/docs/docs/cli/keto-relation-tuple-get.md +++ b/docs/docs/cli/keto-relation-tuple-get.md @@ -33,7 +33,8 @@ keto relation-tuple get <namespace> [flags] -q, --quiet Be quiet with output printing. --read-remote string Remote URL of the read API endpoint. (default "127.0.0.1:4466") --relation string Set the requested relation - --subject string Set the requested subject + --subject-id string Set the requested subject ID + --subject-set string Set the requested subject set; format: "namespace:object#relation" --write-remote string Remote URL of the write API endpoint. (default "127.0.0.1:4467") ``` diff --git a/go.mod b/go.mod index 4351583e1..e562002c8 100644 --- a/go.mod +++ b/go.mod @@ -55,4 +55,155 @@ require ( google.golang.org/protobuf v1.26.0 ) -go 1.16 +require ( + github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect + github.com/DataDog/datadog-go v4.0.0+incompatible // indirect + github.com/Masterminds/semver/v3 v3.1.1 // indirect + github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/armon/go-radix v1.0.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect + github.com/cenkalti/backoff/v4 v4.1.0 // indirect + github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/cockroachdb/cockroach-go/v2 v2.1.1 // indirect + github.com/containerd/containerd v1.4.3 // indirect + github.com/containerd/continuity v0.0.0-20200107194136-26c1120b8d41 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgraph-io/ristretto v0.1.0 // indirect + github.com/docker/distribution v2.7.1+incompatible // indirect + github.com/docker/docker v17.12.0-ce-rc1.0.20201201034508-7d75c1d40d88+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/elastic/go-licenser v0.3.1 // indirect + github.com/elastic/go-sysinfo v1.1.1 // indirect + github.com/elastic/go-windows v1.0.0 // indirect + github.com/fatih/color v1.9.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/go-openapi/analysis v0.20.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.5 // indirect + github.com/go-openapi/loads v0.20.2 // indirect + github.com/go-openapi/spec v0.20.3 // indirect + github.com/go-sql-driver/mysql v1.5.0 // indirect + github.com/go-stack/stack v1.8.0 // indirect + github.com/gobuffalo/envy v1.9.0 // indirect + github.com/gobuffalo/fizz v1.13.1-0.20201104174146-3416f0e6618f // indirect + github.com/gobuffalo/flect v0.2.1 // indirect + github.com/gobuffalo/github_flavored_markdown v1.1.0 // indirect + github.com/gobuffalo/helpers v0.6.1 // indirect + github.com/gobuffalo/here v0.6.0 // indirect + github.com/gobuffalo/nulls v0.3.0 // indirect + github.com/gobuffalo/packd v1.0.0 // indirect + github.com/gobuffalo/plush/v4 v4.0.0 // indirect + github.com/gobuffalo/tags/v3 v3.1.0 // indirect + github.com/gobuffalo/validate/v3 v3.2.0 // indirect + github.com/gofrs/uuid/v3 v3.1.2 // indirect + github.com/gogo/protobuf v1.2.1 // indirect + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/uuid v1.2.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/hashicorp/go-cleanhttp v0.5.1 // indirect + github.com/hashicorp/go-retryablehttp v0.6.8 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/inhies/go-bytesize v0.0.0-20201103132853-d0aed0d254f8 // indirect + github.com/instana/go-sensor v1.29.0 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgconn v1.8.1 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.0.6 // indirect + github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect + github.com/jackc/pgtype v1.7.0 // indirect + github.com/jackc/pgx/v4 v4.11.0 // indirect + github.com/jandelgado/gcov2lcov v1.0.4 // indirect + github.com/jcchavezs/porto v0.1.0 // indirect + github.com/jmoiron/sqlx v1.3.3 // indirect + github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect + github.com/joho/godotenv v1.3.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/knadh/koanf v1.2.2 // indirect + github.com/lib/pq v1.10.1 // indirect + github.com/looplab/fsm v0.1.0 // indirect + github.com/magiconair/properties v1.8.4 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/markbates/pkger v0.17.1 // indirect + github.com/mattn/go-colorable v0.1.6 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect + github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect + github.com/microcosm-cc/bluemonday v1.0.2 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect + github.com/opencontainers/go-digest v1.0.0-rc1 // indirect + github.com/opencontainers/image-spec v1.0.1 // indirect + github.com/opencontainers/runc v1.0.0-rc9 // indirect + github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 // indirect + github.com/openzipkin/zipkin-go v0.2.2 // indirect + github.com/ory/dockertest/v3 v3.6.5 // indirect + github.com/ory/go-acc v0.2.6 // indirect + github.com/ory/viper v1.7.5 // indirect + github.com/pborman/uuid v1.2.1 // indirect + github.com/philhofer/fwd v1.0.0 // indirect + github.com/pkg/profile v1.2.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/procfs v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.5.2 // indirect + github.com/russross/blackfriday/v2 v2.0.1 // indirect + github.com/santhosh-tekuri/jsonschema v1.2.4 // indirect + github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 // indirect + github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 // indirect + github.com/sergi/go-diff v1.1.0 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect + github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect + github.com/spf13/afero v1.5.1 // indirect + github.com/spf13/cast v1.3.2-0.20200723214538-8d17101741c8 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 // indirect + github.com/subosito/gotenv v1.2.0 // indirect + github.com/tidwall/match v1.0.3 // indirect + github.com/tidwall/pretty v1.1.0 // indirect + github.com/tinylib/msgp v1.1.2 // indirect + github.com/uber/jaeger-client-go v2.22.1+incompatible // indirect + github.com/uber/jaeger-lib v2.2.0+incompatible // indirect + github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect + go.elastic.co/apm v1.13.0 // indirect + go.elastic.co/apm/module/apmhttp v1.13.0 // indirect + go.elastic.co/apm/module/apmot v1.13.0 // indirect + go.elastic.co/fastjson v1.1.0 // indirect + go.mongodb.org/mongo-driver v1.4.6 // indirect + go.opentelemetry.io/contrib v0.20.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.20.0 // indirect + go.opentelemetry.io/otel v0.20.0 // indirect + go.opentelemetry.io/otel/metric v0.20.0 // indirect + go.opentelemetry.io/otel/trace v0.20.0 // indirect + go.uber.org/atomic v1.6.0 // indirect + golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect + golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect + golang.org/x/mod v0.4.2 // indirect + golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 // indirect + golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 // indirect + golang.org/x/text v0.3.5 // indirect + golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect + golang.org/x/tools v0.1.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/genproto v0.0.0-20210503173045-b96a97608f20 // indirect + gopkg.in/DataDog/dd-trace-go.v1 v1.27.0 // indirect + gopkg.in/ini.v1 v1.62.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect +) + +go 1.17 diff --git a/internal/check/handler.go b/internal/check/handler.go index 8eb6b5ae9..ece7eb1b4 100644 --- a/internal/check/handler.go +++ b/internal/check/handler.go @@ -84,14 +84,12 @@ type RESTResponse struct { // 500: genericError func (h *Handler) getCheck(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { tuple, err := (&relationtuple.InternalRelationTuple{}).FromURLQuery(r.URL.Query()) - if err != nil { - h.d.Writer().WriteError(w, r, herodot.ErrBadRequest.WithError(err.Error())) - return - } - - if tuple.Subject == nil { + if errors.Is(err, relationtuple.ErrNilSubject) { h.d.Writer().WriteError(w, r, herodot.ErrBadRequest.WithReason("Subject has to be specified.")) return + } else if err != nil { + h.d.Writer().WriteError(w, r, herodot.ErrBadRequest.WithError(err.Error())) + return } allowed, err := h.d.PermissionEngine().SubjectIsAllowed(r.Context(), tuple) diff --git a/internal/check/handler_test.go b/internal/check/handler_test.go index 541131c71..3612cae60 100644 --- a/internal/check/handler_test.go +++ b/internal/check/handler_test.go @@ -23,18 +23,18 @@ import ( ) func assertAllowed(t *testing.T, resp *http.Response) { - assert.Equal(t, http.StatusOK, resp.StatusCode) - body, err := io.ReadAll(resp.Body) require.NoError(t, err) + + assert.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) assert.True(t, gjson.GetBytes(body, "allowed").Bool()) } func assertDenied(t *testing.T, resp *http.Response) { - assert.Equal(t, http.StatusForbidden, resp.StatusCode) - body, err := io.ReadAll(resp.Body) require.NoError(t, err) + + assert.Equal(t, http.StatusForbidden, resp.StatusCode, "%s", body) assert.False(t, gjson.GetBytes(body, "allowed").Bool()) } @@ -72,8 +72,8 @@ func TestRESTHandler(t *testing.T) { t.Run("case=returns denied on unknown namespace", func(t *testing.T) { resp, err := ts.Client().Get(ts.URL + check.RouteBase + "?" + url.Values{ - "namespace": {"not " + nspaces[0].Name}, - "subject": {"foo"}, + "namespace": {"not " + nspaces[0].Name}, + "subject_id": {"foo"}, }.Encode()) require.NoError(t, err) @@ -89,7 +89,9 @@ func TestRESTHandler(t *testing.T) { } require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), rt)) - resp, err := ts.Client().Get(ts.URL + check.RouteBase + "?" + rt.ToURLQuery().Encode()) + q, err := rt.ToURLQuery() + require.NoError(t, err) + resp, err := ts.Client().Get(ts.URL + check.RouteBase + "?" + q.Encode()) require.NoError(t, err) assertAllowed(t, resp) @@ -97,8 +99,8 @@ func TestRESTHandler(t *testing.T) { t.Run("case=returns denied", func(t *testing.T) { resp, err := ts.Client().Get(ts.URL + check.RouteBase + "?" + url.Values{ - "namespace": {nspaces[0].Name}, - "subject": {"foo"}, + "namespace": {nspaces[0].Name}, + "subject_id": {"foo"}, }.Encode()) require.NoError(t, err) diff --git a/internal/e2e/cases_test.go b/internal/e2e/cases_test.go index 3ce004906..826072d12 100644 --- a/internal/e2e/cases_test.go +++ b/internal/e2e/cases_test.go @@ -5,6 +5,8 @@ import ( "strconv" "testing" + "github.com/stretchr/testify/require" + "github.com/ory/herodot" "github.com/ory/keto/internal/x" @@ -42,7 +44,8 @@ func runCases(c client, addNamespace func(*testing.T, ...*namespace.Namespace)) c.createTuple(t, tuple) resp := c.queryTuple(t, &relationtuple.RelationQuery{Namespace: tuple.Namespace}) - assert.Contains(t, resp.RelationTuples, tuple) + require.Len(t, resp.RelationTuples, 1) + assert.Equal(t, tuple, resp.RelationTuples[0]) // try the check API to see whether the tuple is interpreted correctly assert.True(t, c.check(t, tuple)) @@ -85,8 +88,8 @@ func runCases(c client, addNamespace func(*testing.T, ...*namespace.Namespace)) assert.Equal(t, expectedTree.Subject, actualTree.Subject) assert.Equal(t, len(expectedTree.Children), len(actualTree.Children), "expected: %+v; actual: %+v", expectedTree.Children, actualTree.Children) - for _, child := range expectedTree.Children { - assert.Contains(t, actualTree.Children, child) + for i, child := range expectedTree.Children { + assert.Contains(t, actualTree.Children, child, "%+v %+v", actualTree.Children[i], child) } }) @@ -147,12 +150,12 @@ func runCases(c client, addNamespace func(*testing.T, ...*namespace.Namespace)) } c.createTuple(t, rt) - resp := c.queryTuple(t, (*relationtuple.RelationQuery)(rt)) + resp := c.queryTuple(t, rt.ToQuery()) assert.Equal(t, []*relationtuple.InternalRelationTuple{rt}, resp.RelationTuples) c.deleteTuple(t, rt) - resp = c.queryTuple(t, (*relationtuple.RelationQuery)(rt)) + resp = c.queryTuple(t, rt.ToQuery()) assert.Len(t, resp.RelationTuples, 0) }) } diff --git a/internal/e2e/cli_client_test.go b/internal/e2e/cli_client_test.go index e5941d0d4..fa43910fd 100644 --- a/internal/e2e/cli_client_test.go +++ b/internal/e2e/cli_client_test.go @@ -47,8 +47,11 @@ func (g *cliClient) createTuple(t require.TestingT, r *relationtuple.InternalRel func (g *cliClient) assembleQueryFlags(q *relationtuple.RelationQuery, opts []x.PaginationOptionSetter) []string { var flags []string - if q.Subject != nil { - flags = append(flags, "--"+clirelationtuple.FlagSubject, q.Subject.String()) + if q.SubjectID != nil { + flags = append(flags, "--"+clirelationtuple.FlagSubjectID, *q.SubjectID) + } + if q.SubjectSet != nil { + flags = append(flags, "--"+clirelationtuple.FlagSubjectSet, q.SubjectSet.String()) } if q.Relation != "" { flags = append(flags, "--"+clirelationtuple.FlagRelation, q.Relation) diff --git a/internal/e2e/grpc_client_test.go b/internal/e2e/grpc_client_test.go index dc325f2fd..e4f035abe 100644 --- a/internal/e2e/grpc_client_test.go +++ b/internal/e2e/grpc_client_test.go @@ -62,8 +62,8 @@ func (g *grpcClient) queryTuple(t require.TestingT, q *relationtuple.RelationQue Object: q.Object, Relation: q.Relation, } - if q.Subject != nil { - query.Subject = q.Subject.ToProto() + if s := q.Subject(); s != nil { + query.Subject = s.ToProto() } pagination := x.GetPaginationOptions(opts...) @@ -95,8 +95,8 @@ func (g *grpcClient) queryTupleErr(t require.TestingT, expected herodot.DefaultE Object: q.Object, Relation: q.Relation, } - if q.Subject != nil { - query.Subject = q.Subject.ToProto() + if s := q.Subject(); s != nil { + query.Subject = s.ToProto() } pagination := x.GetPaginationOptions(opts...) diff --git a/internal/e2e/rest_client_test.go b/internal/e2e/rest_client_test.go index eee1c59f5..e16115ae4 100644 --- a/internal/e2e/rest_client_test.go +++ b/internal/e2e/rest_client_test.go @@ -63,7 +63,9 @@ func (rc *restClient) createTuple(t require.TestingT, r *relationtuple.InternalR } func (rc *restClient) deleteTuple(t require.TestingT, r *relationtuple.InternalRelationTuple) { - body, code := rc.makeRequest(t, http.MethodDelete, relationtuple.RouteBase+"?"+r.ToURLQuery().Encode(), "", true) + q, err := r.ToURLQuery() + require.NoError(t, err) + body, code := rc.makeRequest(t, http.MethodDelete, relationtuple.RouteBase+"?"+q.Encode(), "", true) require.Equal(t, http.StatusNoContent, code, body) } @@ -107,7 +109,9 @@ func (rc *restClient) queryTupleErr(t require.TestingT, expected herodot.Default } func (rc *restClient) check(t require.TestingT, r *relationtuple.InternalRelationTuple) bool { - bodyGet, codeGet := rc.makeRequest(t, http.MethodGet, fmt.Sprintf("%s?%s", check.RouteBase, r.ToURLQuery().Encode()), "", false) + q, err := r.ToURLQuery() + require.NoError(t, err) + bodyGet, codeGet := rc.makeRequest(t, http.MethodGet, fmt.Sprintf("%s?%s", check.RouteBase, q.Encode()), "", false) var respGet check.RESTResponse require.NoError(t, json.Unmarshal([]byte(bodyGet), &respGet)) diff --git a/internal/e2e/http_client_test.go b/internal/e2e/sdk_client_test.go similarity index 67% rename from internal/e2e/http_client_test.go rename to internal/e2e/sdk_client_test.go index 6f2b06ba8..ca136a73d 100644 --- a/internal/e2e/http_client_test.go +++ b/internal/e2e/sdk_client_test.go @@ -52,39 +52,64 @@ func (c *sdkClient) getWriteClient() *httpclient.OryKeto { } func (c *sdkClient) createTuple(t require.TestingT, r *relationtuple.InternalRelationTuple) { + payload := &models.RelationQuery{ + Namespace: &r.Namespace, + Object: r.Object, + Relation: r.Relation, + } + switch s := r.Subject.(type) { + case *relationtuple.SubjectID: + payload.SubjectID = s.ID + case *relationtuple.SubjectSet: + payload.SubjectSet = &models.SubjectSet{ + Namespace: &s.Namespace, + Object: &s.Object, + Relation: &s.Relation, + } + } + _, err := c.getWriteClient().Write.CreateRelationTuple( write.NewCreateRelationTupleParamsWithTimeout(time.Second). - WithPayload(&models.InternalRelationTuple{ - Namespace: &r.Namespace, - Object: &r.Object, - Relation: &r.Relation, - Subject: (*models.Subject)(pointerx.String(r.Subject.String())), - }), + WithPayload(payload), ) require.NoError(t, err) } func (c *sdkClient) deleteTuple(t require.TestingT, r *relationtuple.InternalRelationTuple) { - _, err := c.getWriteClient().Write.DeleteRelationTuple( - write.NewDeleteRelationTupleParamsWithTimeout(time.Second). - WithNamespace(r.Namespace). - WithObject(r.Object). - WithRelation(r.Relation). - WithSubject(pointerx.String(r.Subject.String())), - ) + params := write.NewDeleteRelationTupleParamsWithTimeout(time.Second). + WithNamespace(r.Namespace). + WithObject(r.Object). + WithRelation(r.Relation) + switch s := r.Subject.(type) { + case *relationtuple.SubjectID: + params = params.WithSubjectID(&s.ID) + case *relationtuple.SubjectSet: + params = params. + WithSubjectSetNamespace(&s.Namespace). + WithSubjectSetObject(&s.Object). + WithSubjectSetRelation(&s.Relation) + } + + _, err := c.getWriteClient().Write.DeleteRelationTuple(params) require.NoError(t, err) } func compileParams(q *relationtuple.RelationQuery, opts []x.PaginationOptionSetter) *read.GetRelationTuplesParams { params := read.NewGetRelationTuplesParams().WithNamespace(q.Namespace) if q.Relation != "" { - params = params.WithRelation(&q.Relation) + params = params.WithRelation(q.Relation) } if q.Object != "" { - params = params.WithObject(&q.Object) + params = params.WithObject(q.Object) + } + if q.SubjectID != nil { + params = params.WithSubjectID(q.SubjectID) } - if q.Subject != nil { - params = params.WithSubject(pointerx.String(q.Subject.String())) + if q.SubjectSet != nil { + params = params. + WithSubjectSetNamespace(&q.SubjectSet.Namespace). + WithSubjectSetObject(&q.SubjectSet.Object). + WithSubjectSetRelation(&q.SubjectSet.Relation) } pagination := x.GetPaginationOptions(opts...) @@ -108,13 +133,19 @@ func (c *sdkClient) queryTuple(t require.TestingT, q *relationtuple.RelationQuer } for i, rt := range resp.Payload.RelationTuples { - sub, err := relationtuple.SubjectFromString(string(*rt.Subject)) - require.NoError(t, err) getResp.RelationTuples[i] = &relationtuple.InternalRelationTuple{ Namespace: *rt.Namespace, Object: *rt.Object, Relation: *rt.Relation, - Subject: sub, + } + if rt.SubjectSet != nil { + getResp.RelationTuples[i].Subject = &relationtuple.SubjectSet{ + Namespace: *rt.SubjectSet.Namespace, + Object: *rt.SubjectSet.Object, + Relation: *rt.SubjectSet.Relation, + } + } else { + getResp.RelationTuples[i].Subject = &relationtuple.SubjectID{ID: rt.SubjectID} } } @@ -135,24 +166,38 @@ func (c *sdkClient) queryTupleErr(t require.TestingT, expected herodot.DefaultEr } func (c *sdkClient) check(t require.TestingT, r *relationtuple.InternalRelationTuple) bool { - resp, err := c.getReadClient().Read.GetCheck( - read.NewGetCheckParamsWithTimeout(time.Second). - WithNamespace(r.Namespace). - WithObject(r.Object). - WithRelation(r.Relation). - WithSubject(pointerx.String(r.Subject.String())), - ) + params := read.NewGetCheckParamsWithTimeout(time.Second). + WithNamespace(r.Namespace). + WithObject(r.Object). + WithRelation(r.Relation) + switch s := r.Subject.(type) { + case *relationtuple.SubjectID: + params = params.WithSubjectID(&s.ID) + case *relationtuple.SubjectSet: + params = params. + WithSubjectSetNamespace(&s.Namespace). + WithSubjectSetObject(&s.Object). + WithSubjectSetRelation(&s.Relation) + } + resp, err := c.getReadClient().Read.GetCheck(params) require.NoError(t, err) return *resp.Payload.Allowed } func buildTree(t require.TestingT, mt *models.ExpandTree) *expand.Tree { - sub, err := relationtuple.SubjectFromString(string(*mt.Subject)) - require.NoError(t, err) et := &expand.Tree{ - Type: expand.NodeType(*mt.Type), - Subject: sub, + Type: expand.NodeType(*mt.Type), } + if mt.SubjectSet != nil { + et.Subject = &relationtuple.SubjectSet{ + Namespace: *mt.SubjectSet.Namespace, + Object: *mt.SubjectSet.Object, + Relation: *mt.SubjectSet.Relation, + } + } else { + et.Subject = &relationtuple.SubjectID{ID: mt.SubjectID} + } + if et.Type != expand.Leaf && len(mt.Children) != 0 { et.Children = make([]*expand.Tree, len(mt.Children)) for i, c := range mt.Children { diff --git a/internal/expand/handler.go b/internal/expand/handler.go index 53fefb4c0..ceb5b1c05 100644 --- a/internal/expand/handler.go +++ b/internal/expand/handler.go @@ -52,7 +52,8 @@ func (h *handler) RegisterWriteGRPC(s *grpc.Server) {} // swagger:parameters getExpand // nolint:deadcode,unused type getExpandRequest struct { - Depth int `json:"max-depth"` + // in:query + MaxDepth int `json:"max-depth"` } // swagger:route GET /expand read getExpand diff --git a/internal/expand/tree.go b/internal/expand/tree.go index 63375705b..2528e1467 100644 --- a/internal/expand/tree.go +++ b/internal/expand/tree.go @@ -22,11 +22,9 @@ const ( Leaf NodeType = "leaf" ) -// swagger:model expandTree +// swagger:ignore type Tree struct { - // required: true - Type NodeType `json:"type"` - // required: true + Type NodeType `json:"type"` Subject relationtuple.Subject `json:"subject"` Children []*Tree `json:"children,omitempty"` } @@ -83,30 +81,86 @@ func NodeTypeFromProto(t acl.NodeType) NodeType { return Leaf } -func (t *Tree) UnmarshalJSON(v []byte) error { - type node struct { - Type NodeType `json:"type"` - Children []*Tree `json:"children,omitempty"` - Subject string `json:"subject"` +// swagger:model expandTree +type node struct { + // required: true + Type NodeType `json:"type"` + Children []*node `json:"children,omitempty"` + SubjectID *string `json:"subject_id,omitempty"` + SubjectSet *relationtuple.SubjectSet `json:"subject_set,omitempty"` +} + +func (n *node) toTree() (*Tree, error) { + t := &Tree{} + if n.SubjectID == nil && n.SubjectSet == nil { + return nil, errors.WithStack(relationtuple.ErrNilSubject) + } else if n.SubjectID != nil && n.SubjectSet != nil { + return nil, errors.WithStack(relationtuple.ErrDuplicateSubject) + } + + if n.SubjectID != nil { + t.Subject = &relationtuple.SubjectID{ID: *n.SubjectID} + } else { + t.Subject = n.SubjectSet + } + + t.Type = n.Type + + if n.Children != nil { + t.Children = make([]*Tree, len(n.Children)) + for i := range n.Children { + var err error + t.Children[i], err = n.Children[i].toTree() + if err != nil { + return nil, err + } + } } - n := &node{} - if err := json.Unmarshal(v, n); err != nil { + return t, nil +} + +func (n *node) fromTree(t *Tree) error { + n.Type = t.Type + n.SubjectID = t.Subject.SubjectID() + n.SubjectSet = t.Subject.SubjectSet() + + if t.Children != nil { + n.Children = make([]*node, len(t.Children)) + for i := range t.Children { + n.Children[i] = &node{} + if err := n.Children[i].fromTree(t.Children[i]); err != nil { + return err + } + } + } + + return nil +} + +func (t *Tree) UnmarshalJSON(v []byte) error { + var n node + if err := json.Unmarshal(v, &n); err != nil { return errors.WithStack(err) } - var err error - t.Subject, err = relationtuple.SubjectFromString(n.Subject) + tt, err := (&n).toTree() if err != nil { return err } - t.Type = n.Type - t.Children = n.Children - + *t = *tt return nil } +func (t *Tree) MarshalJSON() ([]byte, error) { + var n node + if err := n.fromTree(t); err != nil { + return nil, err + } + return json.Marshal(n) +} + // swagger:ignore func (t *Tree) ToProto() *acl.SubjectTree { if t == nil { diff --git a/internal/httpclient/client/read/get_check_parameters.go b/internal/httpclient/client/read/get_check_parameters.go index 82a431505..4217ea64b 100644 --- a/internal/httpclient/client/read/get_check_parameters.go +++ b/internal/httpclient/client/read/get_check_parameters.go @@ -77,13 +77,29 @@ type GetCheckParams struct { */ Relation string - /* Subject. + /* SubjectID. - Subject of the Relation Tuple + SubjectID of the Relation Tuple + */ + SubjectID *string + + /* SubjectSetNamespace. - The subject follows the subject string encoding format. + Namespace of the Subject Set */ - Subject *string + SubjectSetNamespace *string + + /* SubjectSetObject. + + Object of the Subject Set + */ + SubjectSetObject *string + + /* SubjectSetRelation. + + Relation of the Subject Set + */ + SubjectSetRelation *string timeout time.Duration Context context.Context @@ -171,15 +187,48 @@ func (o *GetCheckParams) SetRelation(relation string) { o.Relation = relation } -// WithSubject adds the subject to the get check params -func (o *GetCheckParams) WithSubject(subject *string) *GetCheckParams { - o.SetSubject(subject) +// WithSubjectID adds the subjectID to the get check params +func (o *GetCheckParams) WithSubjectID(subjectID *string) *GetCheckParams { + o.SetSubjectID(subjectID) + return o +} + +// SetSubjectID adds the subjectId to the get check params +func (o *GetCheckParams) SetSubjectID(subjectID *string) { + o.SubjectID = subjectID +} + +// WithSubjectSetNamespace adds the subjectSetNamespace to the get check params +func (o *GetCheckParams) WithSubjectSetNamespace(subjectSetNamespace *string) *GetCheckParams { + o.SetSubjectSetNamespace(subjectSetNamespace) return o } -// SetSubject adds the subject to the get check params -func (o *GetCheckParams) SetSubject(subject *string) { - o.Subject = subject +// SetSubjectSetNamespace adds the subjectSetNamespace to the get check params +func (o *GetCheckParams) SetSubjectSetNamespace(subjectSetNamespace *string) { + o.SubjectSetNamespace = subjectSetNamespace +} + +// WithSubjectSetObject adds the subjectSetObject to the get check params +func (o *GetCheckParams) WithSubjectSetObject(subjectSetObject *string) *GetCheckParams { + o.SetSubjectSetObject(subjectSetObject) + return o +} + +// SetSubjectSetObject adds the subjectSetObject to the get check params +func (o *GetCheckParams) SetSubjectSetObject(subjectSetObject *string) { + o.SubjectSetObject = subjectSetObject +} + +// WithSubjectSetRelation adds the subjectSetRelation to the get check params +func (o *GetCheckParams) WithSubjectSetRelation(subjectSetRelation *string) *GetCheckParams { + o.SetSubjectSetRelation(subjectSetRelation) + return o +} + +// SetSubjectSetRelation adds the subjectSetRelation to the get check params +func (o *GetCheckParams) SetSubjectSetRelation(subjectSetRelation *string) { + o.SubjectSetRelation = subjectSetRelation } // WriteToRequest writes these params to a swagger request @@ -220,18 +269,69 @@ func (o *GetCheckParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Regi } } - if o.Subject != nil { + if o.SubjectID != nil { + + // query param subject_id + var qrSubjectID string + + if o.SubjectID != nil { + qrSubjectID = *o.SubjectID + } + qSubjectID := qrSubjectID + if qSubjectID != "" { + + if err := r.SetQueryParam("subject_id", qSubjectID); err != nil { + return err + } + } + } + + if o.SubjectSetNamespace != nil { + + // query param subject_set.namespace + var qrSubjectSetNamespace string + + if o.SubjectSetNamespace != nil { + qrSubjectSetNamespace = *o.SubjectSetNamespace + } + qSubjectSetNamespace := qrSubjectSetNamespace + if qSubjectSetNamespace != "" { + + if err := r.SetQueryParam("subject_set.namespace", qSubjectSetNamespace); err != nil { + return err + } + } + } + + if o.SubjectSetObject != nil { + + // query param subject_set.object + var qrSubjectSetObject string + + if o.SubjectSetObject != nil { + qrSubjectSetObject = *o.SubjectSetObject + } + qSubjectSetObject := qrSubjectSetObject + if qSubjectSetObject != "" { + + if err := r.SetQueryParam("subject_set.object", qSubjectSetObject); err != nil { + return err + } + } + } + + if o.SubjectSetRelation != nil { - // query param subject - var qrSubject string + // query param subject_set.relation + var qrSubjectSetRelation string - if o.Subject != nil { - qrSubject = *o.Subject + if o.SubjectSetRelation != nil { + qrSubjectSetRelation = *o.SubjectSetRelation } - qSubject := qrSubject - if qSubject != "" { + qSubjectSetRelation := qrSubjectSetRelation + if qSubjectSetRelation != "" { - if err := r.SetQueryParam("subject", qSubject); err != nil { + if err := r.SetQueryParam("subject_set.relation", qSubjectSetRelation); err != nil { return err } } diff --git a/internal/httpclient/client/read/get_expand_parameters.go b/internal/httpclient/client/read/get_expand_parameters.go index ea27fc0f5..5e81db9cf 100644 --- a/internal/httpclient/client/read/get_expand_parameters.go +++ b/internal/httpclient/client/read/get_expand_parameters.go @@ -67,19 +67,19 @@ type GetExpandParams struct { /* Namespace. - Namespace of the Relation Tuple + Namespace of the Subject Set */ Namespace string /* Object. - Object of the Relation Tuple + Object of the Subject Set */ Object string /* Relation. - Relation of the Relation Tuple + Relation of the Subject Set */ Relation string diff --git a/internal/httpclient/client/read/get_relation_tuples_parameters.go b/internal/httpclient/client/read/get_relation_tuples_parameters.go index 1e2d6ae1a..e719e1ad9 100644 --- a/internal/httpclient/client/read/get_relation_tuples_parameters.go +++ b/internal/httpclient/client/read/get_relation_tuples_parameters.go @@ -60,11 +60,17 @@ func NewGetRelationTuplesParamsWithHTTPClient(client *http.Client) *GetRelationT */ type GetRelationTuplesParams struct { - // Namespace. + /* Namespace. + + Namespace of the Relation Tuple + */ Namespace string - // Object. - Object *string + /* Object. + + Object of the Relation Tuple + */ + Object string // PageSize. // @@ -74,11 +80,35 @@ type GetRelationTuplesParams struct { // PageToken. PageToken *string - // Relation. - Relation *string + /* Relation. + + Relation of the Relation Tuple + */ + Relation string + + /* SubjectID. + + SubjectID of the Relation Tuple + */ + SubjectID *string + + /* SubjectSetNamespace. + + Namespace of the Subject Set + */ + SubjectSetNamespace *string - // Subject. - Subject *string + /* SubjectSetObject. + + Object of the Subject Set + */ + SubjectSetObject *string + + /* SubjectSetRelation. + + Relation of the Subject Set + */ + SubjectSetRelation *string timeout time.Duration Context context.Context @@ -145,13 +175,13 @@ func (o *GetRelationTuplesParams) SetNamespace(namespace string) { } // WithObject adds the object to the get relation tuples params -func (o *GetRelationTuplesParams) WithObject(object *string) *GetRelationTuplesParams { +func (o *GetRelationTuplesParams) WithObject(object string) *GetRelationTuplesParams { o.SetObject(object) return o } // SetObject adds the object to the get relation tuples params -func (o *GetRelationTuplesParams) SetObject(object *string) { +func (o *GetRelationTuplesParams) SetObject(object string) { o.Object = object } @@ -178,25 +208,58 @@ func (o *GetRelationTuplesParams) SetPageToken(pageToken *string) { } // WithRelation adds the relation to the get relation tuples params -func (o *GetRelationTuplesParams) WithRelation(relation *string) *GetRelationTuplesParams { +func (o *GetRelationTuplesParams) WithRelation(relation string) *GetRelationTuplesParams { o.SetRelation(relation) return o } // SetRelation adds the relation to the get relation tuples params -func (o *GetRelationTuplesParams) SetRelation(relation *string) { +func (o *GetRelationTuplesParams) SetRelation(relation string) { o.Relation = relation } -// WithSubject adds the subject to the get relation tuples params -func (o *GetRelationTuplesParams) WithSubject(subject *string) *GetRelationTuplesParams { - o.SetSubject(subject) +// WithSubjectID adds the subjectID to the get relation tuples params +func (o *GetRelationTuplesParams) WithSubjectID(subjectID *string) *GetRelationTuplesParams { + o.SetSubjectID(subjectID) return o } -// SetSubject adds the subject to the get relation tuples params -func (o *GetRelationTuplesParams) SetSubject(subject *string) { - o.Subject = subject +// SetSubjectID adds the subjectId to the get relation tuples params +func (o *GetRelationTuplesParams) SetSubjectID(subjectID *string) { + o.SubjectID = subjectID +} + +// WithSubjectSetNamespace adds the subjectSetNamespace to the get relation tuples params +func (o *GetRelationTuplesParams) WithSubjectSetNamespace(subjectSetNamespace *string) *GetRelationTuplesParams { + o.SetSubjectSetNamespace(subjectSetNamespace) + return o +} + +// SetSubjectSetNamespace adds the subjectSetNamespace to the get relation tuples params +func (o *GetRelationTuplesParams) SetSubjectSetNamespace(subjectSetNamespace *string) { + o.SubjectSetNamespace = subjectSetNamespace +} + +// WithSubjectSetObject adds the subjectSetObject to the get relation tuples params +func (o *GetRelationTuplesParams) WithSubjectSetObject(subjectSetObject *string) *GetRelationTuplesParams { + o.SetSubjectSetObject(subjectSetObject) + return o +} + +// SetSubjectSetObject adds the subjectSetObject to the get relation tuples params +func (o *GetRelationTuplesParams) SetSubjectSetObject(subjectSetObject *string) { + o.SubjectSetObject = subjectSetObject +} + +// WithSubjectSetRelation adds the subjectSetRelation to the get relation tuples params +func (o *GetRelationTuplesParams) WithSubjectSetRelation(subjectSetRelation *string) *GetRelationTuplesParams { + o.SetSubjectSetRelation(subjectSetRelation) + return o +} + +// SetSubjectSetRelation adds the subjectSetRelation to the get relation tuples params +func (o *GetRelationTuplesParams) SetSubjectSetRelation(subjectSetRelation *string) { + o.SubjectSetRelation = subjectSetRelation } // WriteToRequest writes these params to a swagger request @@ -217,20 +280,13 @@ func (o *GetRelationTuplesParams) WriteToRequest(r runtime.ClientRequest, reg st } } - if o.Object != nil { - - // query param object - var qrObject string - - if o.Object != nil { - qrObject = *o.Object - } - qObject := qrObject - if qObject != "" { + // query param object + qrObject := o.Object + qObject := qrObject + if qObject != "" { - if err := r.SetQueryParam("object", qObject); err != nil { - return err - } + if err := r.SetQueryParam("object", qObject); err != nil { + return err } } @@ -268,35 +324,79 @@ func (o *GetRelationTuplesParams) WriteToRequest(r runtime.ClientRequest, reg st } } - if o.Relation != nil { + // query param relation + qrRelation := o.Relation + qRelation := qrRelation + if qRelation != "" { + + if err := r.SetQueryParam("relation", qRelation); err != nil { + return err + } + } + + if o.SubjectID != nil { + + // query param subject_id + var qrSubjectID string + + if o.SubjectID != nil { + qrSubjectID = *o.SubjectID + } + qSubjectID := qrSubjectID + if qSubjectID != "" { + + if err := r.SetQueryParam("subject_id", qSubjectID); err != nil { + return err + } + } + } + + if o.SubjectSetNamespace != nil { + + // query param subject_set.namespace + var qrSubjectSetNamespace string + + if o.SubjectSetNamespace != nil { + qrSubjectSetNamespace = *o.SubjectSetNamespace + } + qSubjectSetNamespace := qrSubjectSetNamespace + if qSubjectSetNamespace != "" { + + if err := r.SetQueryParam("subject_set.namespace", qSubjectSetNamespace); err != nil { + return err + } + } + } + + if o.SubjectSetObject != nil { - // query param relation - var qrRelation string + // query param subject_set.object + var qrSubjectSetObject string - if o.Relation != nil { - qrRelation = *o.Relation + if o.SubjectSetObject != nil { + qrSubjectSetObject = *o.SubjectSetObject } - qRelation := qrRelation - if qRelation != "" { + qSubjectSetObject := qrSubjectSetObject + if qSubjectSetObject != "" { - if err := r.SetQueryParam("relation", qRelation); err != nil { + if err := r.SetQueryParam("subject_set.object", qSubjectSetObject); err != nil { return err } } } - if o.Subject != nil { + if o.SubjectSetRelation != nil { - // query param subject - var qrSubject string + // query param subject_set.relation + var qrSubjectSetRelation string - if o.Subject != nil { - qrSubject = *o.Subject + if o.SubjectSetRelation != nil { + qrSubjectSetRelation = *o.SubjectSetRelation } - qSubject := qrSubject - if qSubject != "" { + qSubjectSetRelation := qrSubjectSetRelation + if qSubjectSetRelation != "" { - if err := r.SetQueryParam("subject", qSubject); err != nil { + if err := r.SetQueryParam("subject_set.relation", qSubjectSetRelation); err != nil { return err } } diff --git a/internal/httpclient/client/read/post_check_parameters.go b/internal/httpclient/client/read/post_check_parameters.go index 4c43f0b0b..1b423a5ab 100644 --- a/internal/httpclient/client/read/post_check_parameters.go +++ b/internal/httpclient/client/read/post_check_parameters.go @@ -62,7 +62,7 @@ func NewPostCheckParamsWithHTTPClient(client *http.Client) *PostCheckParams { type PostCheckParams struct { // Payload. - Payload *models.InternalRelationTuple + Payload *models.RelationQuery timeout time.Duration Context context.Context @@ -118,13 +118,13 @@ func (o *PostCheckParams) SetHTTPClient(client *http.Client) { } // WithPayload adds the payload to the post check params -func (o *PostCheckParams) WithPayload(payload *models.InternalRelationTuple) *PostCheckParams { +func (o *PostCheckParams) WithPayload(payload *models.RelationQuery) *PostCheckParams { o.SetPayload(payload) return o } // SetPayload adds the payload to the post check params -func (o *PostCheckParams) SetPayload(payload *models.InternalRelationTuple) { +func (o *PostCheckParams) SetPayload(payload *models.RelationQuery) { o.Payload = payload } diff --git a/internal/httpclient/client/write/create_relation_tuple_parameters.go b/internal/httpclient/client/write/create_relation_tuple_parameters.go index 61e5625cc..3be845ad1 100644 --- a/internal/httpclient/client/write/create_relation_tuple_parameters.go +++ b/internal/httpclient/client/write/create_relation_tuple_parameters.go @@ -62,7 +62,7 @@ func NewCreateRelationTupleParamsWithHTTPClient(client *http.Client) *CreateRela type CreateRelationTupleParams struct { // Payload. - Payload *models.InternalRelationTuple + Payload *models.RelationQuery timeout time.Duration Context context.Context @@ -118,13 +118,13 @@ func (o *CreateRelationTupleParams) SetHTTPClient(client *http.Client) { } // WithPayload adds the payload to the create relation tuple params -func (o *CreateRelationTupleParams) WithPayload(payload *models.InternalRelationTuple) *CreateRelationTupleParams { +func (o *CreateRelationTupleParams) WithPayload(payload *models.RelationQuery) *CreateRelationTupleParams { o.SetPayload(payload) return o } // SetPayload adds the payload to the create relation tuple params -func (o *CreateRelationTupleParams) SetPayload(payload *models.InternalRelationTuple) { +func (o *CreateRelationTupleParams) SetPayload(payload *models.RelationQuery) { o.Payload = payload } diff --git a/internal/httpclient/client/write/create_relation_tuple_responses.go b/internal/httpclient/client/write/create_relation_tuple_responses.go index 4ffbe233d..e3b365f3c 100644 --- a/internal/httpclient/client/write/create_relation_tuple_responses.go +++ b/internal/httpclient/client/write/create_relation_tuple_responses.go @@ -55,22 +55,22 @@ func NewCreateRelationTupleCreated() *CreateRelationTupleCreated { /* CreateRelationTupleCreated describes a response with status code 201, with default header values. -InternalRelationTuple +RelationQuery */ type CreateRelationTupleCreated struct { - Payload *models.InternalRelationTuple + Payload *models.RelationQuery } func (o *CreateRelationTupleCreated) Error() string { return fmt.Sprintf("[PUT /relation-tuples][%d] createRelationTupleCreated %+v", 201, o.Payload) } -func (o *CreateRelationTupleCreated) GetPayload() *models.InternalRelationTuple { +func (o *CreateRelationTupleCreated) GetPayload() *models.RelationQuery { return o.Payload } func (o *CreateRelationTupleCreated) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { - o.Payload = new(models.InternalRelationTuple) + o.Payload = new(models.RelationQuery) // response payload if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { diff --git a/internal/httpclient/client/write/delete_relation_tuple_parameters.go b/internal/httpclient/client/write/delete_relation_tuple_parameters.go index 415ec6836..f0117da4d 100644 --- a/internal/httpclient/client/write/delete_relation_tuple_parameters.go +++ b/internal/httpclient/client/write/delete_relation_tuple_parameters.go @@ -77,13 +77,29 @@ type DeleteRelationTupleParams struct { */ Relation string - /* Subject. + /* SubjectID. - Subject of the Relation Tuple + SubjectID of the Relation Tuple + */ + SubjectID *string + + /* SubjectSetNamespace. - The subject follows the subject string encoding format. + Namespace of the Subject Set */ - Subject *string + SubjectSetNamespace *string + + /* SubjectSetObject. + + Object of the Subject Set + */ + SubjectSetObject *string + + /* SubjectSetRelation. + + Relation of the Subject Set + */ + SubjectSetRelation *string timeout time.Duration Context context.Context @@ -171,15 +187,48 @@ func (o *DeleteRelationTupleParams) SetRelation(relation string) { o.Relation = relation } -// WithSubject adds the subject to the delete relation tuple params -func (o *DeleteRelationTupleParams) WithSubject(subject *string) *DeleteRelationTupleParams { - o.SetSubject(subject) +// WithSubjectID adds the subjectID to the delete relation tuple params +func (o *DeleteRelationTupleParams) WithSubjectID(subjectID *string) *DeleteRelationTupleParams { + o.SetSubjectID(subjectID) + return o +} + +// SetSubjectID adds the subjectId to the delete relation tuple params +func (o *DeleteRelationTupleParams) SetSubjectID(subjectID *string) { + o.SubjectID = subjectID +} + +// WithSubjectSetNamespace adds the subjectSetNamespace to the delete relation tuple params +func (o *DeleteRelationTupleParams) WithSubjectSetNamespace(subjectSetNamespace *string) *DeleteRelationTupleParams { + o.SetSubjectSetNamespace(subjectSetNamespace) return o } -// SetSubject adds the subject to the delete relation tuple params -func (o *DeleteRelationTupleParams) SetSubject(subject *string) { - o.Subject = subject +// SetSubjectSetNamespace adds the subjectSetNamespace to the delete relation tuple params +func (o *DeleteRelationTupleParams) SetSubjectSetNamespace(subjectSetNamespace *string) { + o.SubjectSetNamespace = subjectSetNamespace +} + +// WithSubjectSetObject adds the subjectSetObject to the delete relation tuple params +func (o *DeleteRelationTupleParams) WithSubjectSetObject(subjectSetObject *string) *DeleteRelationTupleParams { + o.SetSubjectSetObject(subjectSetObject) + return o +} + +// SetSubjectSetObject adds the subjectSetObject to the delete relation tuple params +func (o *DeleteRelationTupleParams) SetSubjectSetObject(subjectSetObject *string) { + o.SubjectSetObject = subjectSetObject +} + +// WithSubjectSetRelation adds the subjectSetRelation to the delete relation tuple params +func (o *DeleteRelationTupleParams) WithSubjectSetRelation(subjectSetRelation *string) *DeleteRelationTupleParams { + o.SetSubjectSetRelation(subjectSetRelation) + return o +} + +// SetSubjectSetRelation adds the subjectSetRelation to the delete relation tuple params +func (o *DeleteRelationTupleParams) SetSubjectSetRelation(subjectSetRelation *string) { + o.SubjectSetRelation = subjectSetRelation } // WriteToRequest writes these params to a swagger request @@ -220,18 +269,69 @@ func (o *DeleteRelationTupleParams) WriteToRequest(r runtime.ClientRequest, reg } } - if o.Subject != nil { + if o.SubjectID != nil { + + // query param subject_id + var qrSubjectID string + + if o.SubjectID != nil { + qrSubjectID = *o.SubjectID + } + qSubjectID := qrSubjectID + if qSubjectID != "" { + + if err := r.SetQueryParam("subject_id", qSubjectID); err != nil { + return err + } + } + } + + if o.SubjectSetNamespace != nil { + + // query param subject_set.namespace + var qrSubjectSetNamespace string + + if o.SubjectSetNamespace != nil { + qrSubjectSetNamespace = *o.SubjectSetNamespace + } + qSubjectSetNamespace := qrSubjectSetNamespace + if qSubjectSetNamespace != "" { + + if err := r.SetQueryParam("subject_set.namespace", qSubjectSetNamespace); err != nil { + return err + } + } + } + + if o.SubjectSetObject != nil { + + // query param subject_set.object + var qrSubjectSetObject string + + if o.SubjectSetObject != nil { + qrSubjectSetObject = *o.SubjectSetObject + } + qSubjectSetObject := qrSubjectSetObject + if qSubjectSetObject != "" { + + if err := r.SetQueryParam("subject_set.object", qSubjectSetObject); err != nil { + return err + } + } + } + + if o.SubjectSetRelation != nil { - // query param subject - var qrSubject string + // query param subject_set.relation + var qrSubjectSetRelation string - if o.Subject != nil { - qrSubject = *o.Subject + if o.SubjectSetRelation != nil { + qrSubjectSetRelation = *o.SubjectSetRelation } - qSubject := qrSubject - if qSubject != "" { + qSubjectSetRelation := qrSubjectSetRelation + if qSubjectSetRelation != "" { - if err := r.SetQueryParam("subject", qSubject); err != nil { + if err := r.SetQueryParam("subject_set.relation", qSubjectSetRelation); err != nil { return err } } diff --git a/internal/httpclient/models/expand_tree.go b/internal/httpclient/models/expand_tree.go index ad1a0d33d..986d71d19 100644 --- a/internal/httpclient/models/expand_tree.go +++ b/internal/httpclient/models/expand_tree.go @@ -24,9 +24,11 @@ type ExpandTree struct { // children Children []*ExpandTree `json:"children"` - // subject - // Required: true - Subject *Subject `json:"subject"` + // subject id + SubjectID string `json:"subject_id,omitempty"` + + // subject set + SubjectSet *SubjectSet `json:"subject_set,omitempty"` // type // Required: true @@ -42,7 +44,7 @@ func (m *ExpandTree) Validate(formats strfmt.Registry) error { res = append(res, err) } - if err := m.validateSubject(formats); err != nil { + if err := m.validateSubjectSet(formats); err != nil { res = append(res, err) } @@ -80,20 +82,15 @@ func (m *ExpandTree) validateChildren(formats strfmt.Registry) error { return nil } -func (m *ExpandTree) validateSubject(formats strfmt.Registry) error { - - if err := validate.Required("subject", "body", m.Subject); err != nil { - return err - } - - if err := validate.Required("subject", "body", m.Subject); err != nil { - return err +func (m *ExpandTree) validateSubjectSet(formats strfmt.Registry) error { + if swag.IsZero(m.SubjectSet) { // not required + return nil } - if m.Subject != nil { - if err := m.Subject.Validate(formats); err != nil { + if m.SubjectSet != nil { + if err := m.SubjectSet.Validate(formats); err != nil { if ve, ok := err.(*errors.Validation); ok { - return ve.ValidateName("subject") + return ve.ValidateName("subject_set") } return err } @@ -159,7 +156,7 @@ func (m *ExpandTree) ContextValidate(ctx context.Context, formats strfmt.Registr res = append(res, err) } - if err := m.contextValidateSubject(ctx, formats); err != nil { + if err := m.contextValidateSubjectSet(ctx, formats); err != nil { res = append(res, err) } @@ -187,12 +184,12 @@ func (m *ExpandTree) contextValidateChildren(ctx context.Context, formats strfmt return nil } -func (m *ExpandTree) contextValidateSubject(ctx context.Context, formats strfmt.Registry) error { +func (m *ExpandTree) contextValidateSubjectSet(ctx context.Context, formats strfmt.Registry) error { - if m.Subject != nil { - if err := m.Subject.ContextValidate(ctx, formats); err != nil { + if m.SubjectSet != nil { + if err := m.SubjectSet.ContextValidate(ctx, formats); err != nil { if ve, ok := err.(*errors.Validation); ok { - return ve.ValidateName("subject") + return ve.ValidateName("subject_set") } return err } diff --git a/internal/httpclient/models/internal_relation_tuple.go b/internal/httpclient/models/internal_relation_tuple.go index b002433d5..e17517c99 100644 --- a/internal/httpclient/models/internal_relation_tuple.go +++ b/internal/httpclient/models/internal_relation_tuple.go @@ -20,26 +20,24 @@ import ( type InternalRelationTuple struct { // Namespace of the Relation Tuple - // - // in: query // Required: true Namespace *string `json:"namespace"` // Object of the Relation Tuple - // - // in: query // Required: true Object *string `json:"object"` // Relation of the Relation Tuple - // - // in: query // Required: true Relation *string `json:"relation"` - // subject - // Required: true - Subject *Subject `json:"subject"` + // SubjectID of the Relation Tuple + // + // Either SubjectSet or SubjectID are required. + SubjectID string `json:"subject_id,omitempty"` + + // subject set + SubjectSet *SubjectSet `json:"subject_set,omitempty"` } // Validate validates this internal relation tuple @@ -58,7 +56,7 @@ func (m *InternalRelationTuple) Validate(formats strfmt.Registry) error { res = append(res, err) } - if err := m.validateSubject(formats); err != nil { + if err := m.validateSubjectSet(formats); err != nil { res = append(res, err) } @@ -95,20 +93,15 @@ func (m *InternalRelationTuple) validateRelation(formats strfmt.Registry) error return nil } -func (m *InternalRelationTuple) validateSubject(formats strfmt.Registry) error { - - if err := validate.Required("subject", "body", m.Subject); err != nil { - return err - } - - if err := validate.Required("subject", "body", m.Subject); err != nil { - return err +func (m *InternalRelationTuple) validateSubjectSet(formats strfmt.Registry) error { + if swag.IsZero(m.SubjectSet) { // not required + return nil } - if m.Subject != nil { - if err := m.Subject.Validate(formats); err != nil { + if m.SubjectSet != nil { + if err := m.SubjectSet.Validate(formats); err != nil { if ve, ok := err.(*errors.Validation); ok { - return ve.ValidateName("subject") + return ve.ValidateName("subject_set") } return err } @@ -121,7 +114,7 @@ func (m *InternalRelationTuple) validateSubject(formats strfmt.Registry) error { func (m *InternalRelationTuple) ContextValidate(ctx context.Context, formats strfmt.Registry) error { var res []error - if err := m.contextValidateSubject(ctx, formats); err != nil { + if err := m.contextValidateSubjectSet(ctx, formats); err != nil { res = append(res, err) } @@ -131,12 +124,12 @@ func (m *InternalRelationTuple) ContextValidate(ctx context.Context, formats str return nil } -func (m *InternalRelationTuple) contextValidateSubject(ctx context.Context, formats strfmt.Registry) error { +func (m *InternalRelationTuple) contextValidateSubjectSet(ctx context.Context, formats strfmt.Registry) error { - if m.Subject != nil { - if err := m.Subject.ContextValidate(ctx, formats); err != nil { + if m.SubjectSet != nil { + if err := m.SubjectSet.ContextValidate(ctx, formats); err != nil { if ve, ok := err.(*errors.Validation); ok { - return ve.ValidateName("subject") + return ve.ValidateName("subject_set") } return err } diff --git a/internal/httpclient/models/relation_query.go b/internal/httpclient/models/relation_query.go new file mode 100644 index 000000000..e07d68ce3 --- /dev/null +++ b/internal/httpclient/models/relation_query.go @@ -0,0 +1,129 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// RelationQuery relation query +// +// swagger:model RelationQuery +type RelationQuery struct { + + // Namespace of the Relation Tuple + // Required: true + Namespace *string `json:"namespace"` + + // Object of the Relation Tuple + Object string `json:"object,omitempty"` + + // Relation of the Relation Tuple + Relation string `json:"relation,omitempty"` + + // SubjectID of the Relation Tuple + // + // Either SubjectSet or SubjectID can be provided. + SubjectID string `json:"subject_id,omitempty"` + + // subject set + SubjectSet *SubjectSet `json:"subject_set,omitempty"` +} + +// Validate validates this relation query +func (m *RelationQuery) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateNamespace(formats); err != nil { + res = append(res, err) + } + + if err := m.validateSubjectSet(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *RelationQuery) validateNamespace(formats strfmt.Registry) error { + + if err := validate.Required("namespace", "body", m.Namespace); err != nil { + return err + } + + return nil +} + +func (m *RelationQuery) validateSubjectSet(formats strfmt.Registry) error { + if swag.IsZero(m.SubjectSet) { // not required + return nil + } + + if m.SubjectSet != nil { + if err := m.SubjectSet.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("subject_set") + } + return err + } + } + + return nil +} + +// ContextValidate validate this relation query based on the context it is used +func (m *RelationQuery) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateSubjectSet(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *RelationQuery) contextValidateSubjectSet(ctx context.Context, formats strfmt.Registry) error { + + if m.SubjectSet != nil { + if err := m.SubjectSet.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("subject_set") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *RelationQuery) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *RelationQuery) UnmarshalBinary(b []byte) error { + var res RelationQuery + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/internal/httpclient/models/subject.go b/internal/httpclient/models/subject.go deleted file mode 100644 index 2c079333c..000000000 --- a/internal/httpclient/models/subject.go +++ /dev/null @@ -1,27 +0,0 @@ -// Code generated by go-swagger; DO NOT EDIT. - -package models - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -import ( - "context" - - "github.com/go-openapi/strfmt" -) - -// Subject subject -// -// swagger:model subject -type Subject string - -// Validate validates this subject -func (m Subject) Validate(formats strfmt.Registry) error { - return nil -} - -// ContextValidate validates this subject based on context it is used -func (m Subject) ContextValidate(ctx context.Context, formats strfmt.Registry) error { - return nil -} diff --git a/internal/httpclient/models/subject_set.go b/internal/httpclient/models/subject_set.go new file mode 100644 index 000000000..d6a2b3999 --- /dev/null +++ b/internal/httpclient/models/subject_set.go @@ -0,0 +1,105 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// SubjectSet subject set +// +// swagger:model SubjectSet +type SubjectSet struct { + + // Namespace of the Subject Set + // Required: true + Namespace *string `json:"namespace"` + + // Object of the Subject Set + // Required: true + Object *string `json:"object"` + + // Relation of the Subject Set + // Required: true + Relation *string `json:"relation"` +} + +// Validate validates this subject set +func (m *SubjectSet) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateNamespace(formats); err != nil { + res = append(res, err) + } + + if err := m.validateObject(formats); err != nil { + res = append(res, err) + } + + if err := m.validateRelation(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *SubjectSet) validateNamespace(formats strfmt.Registry) error { + + if err := validate.Required("namespace", "body", m.Namespace); err != nil { + return err + } + + return nil +} + +func (m *SubjectSet) validateObject(formats strfmt.Registry) error { + + if err := validate.Required("object", "body", m.Object); err != nil { + return err + } + + return nil +} + +func (m *SubjectSet) validateRelation(formats strfmt.Registry) error { + + if err := validate.Required("relation", "body", m.Relation); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this subject set based on context it is used +func (m *SubjectSet) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *SubjectSet) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *SubjectSet) UnmarshalBinary(b []byte) error { + var res SubjectSet + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/internal/persistence/sql/relationtuples.go b/internal/persistence/sql/relationtuples.go index 608531671..bd46e213a 100644 --- a/internal/persistence/sql/relationtuples.go +++ b/internal/persistence/sql/relationtuples.go @@ -221,8 +221,8 @@ func (p *Persister) GetRelationTuples(ctx context.Context, query *relationtuple. if query.Object != "" { sqlQuery.Where("object = ?", query.Object) } - if query.Subject != nil { - if err := p.whereSubject(ctx, sqlQuery, query.Subject); err != nil { + if s := query.Subject(); s != nil { + if err := p.whereSubject(ctx, sqlQuery, s); err != nil { return nil, "", err } } diff --git a/internal/relationtuple/definitions.go b/internal/relationtuple/definitions.go index b0cffbd93..0bed1fc4f 100644 --- a/internal/relationtuple/definitions.go +++ b/internal/relationtuple/definitions.go @@ -8,19 +8,17 @@ import ( "strings" "testing" + "github.com/ory/x/pointerx" + "github.com/ory/herodot" "github.com/sirupsen/logrus" acl "github.com/ory/keto/proto/ory/keto/acl/v1alpha1" - "github.com/tidwall/sjson" - "github.com/pkg/errors" "github.com/ory/keto/internal/x" - - "github.com/tidwall/gjson" ) type ( @@ -44,11 +42,27 @@ type ( ) type RelationQuery struct { + // Namespace of the Relation Tuple + // // required: true - Namespace string `json:"namespace"` - Object string `json:"object"` - Relation string `json:"relation"` - Subject Subject `json:"subject"` + Namespace string `json:"namespace"` + + // Object of the Relation Tuple + Object string `json:"object"` + + // Relation of the Relation Tuple + Relation string `json:"relation"` + + // SubjectID of the Relation Tuple + // + // Either SubjectSet or SubjectID can be provided. + SubjectID *string `json:"subject_id,omitempty"` + // SubjectSet of the Relation Tuple + // + // Either SubjectSet or SubjectID can be provided. + // + // swagger:allOf + SubjectSet *SubjectSet `json:"subject_set,omitempty"` } // swagger:ignore @@ -62,70 +76,43 @@ type TupleData interface { // swagger:model subject type Subject interface { - // swagger:ignore - json.Marshaler - // swagger:ignore String() string // swagger:ignore FromString(string) (Subject, error) // swagger:ignore Equals(interface{}) bool + // swagger:ignore + SubjectID() *string + // swagger:ignore + SubjectSet() *SubjectSet // swagger:ignore ToProto() *acl.Subject } -// swagger:parameters getCheck deleteRelationTuple +// swagger:ignore type InternalRelationTuple struct { - // Namespace of the Relation Tuple - // - // in: query - // required: true - Namespace string `json:"namespace"` - - // Object of the Relation Tuple - // - // in: query - // required: true - Object string `json:"object"` - - // Relation of the Relation Tuple - // - // in: query - // required: true - Relation string `json:"relation"` - - // Subject of the Relation Tuple - // - // The subject follows the subject string encoding format. - // - // in: query - // required: true - Subject Subject `json:"subject"` + Namespace string `json:"namespace"` + Object string `json:"object"` + Relation string `json:"relation"` + Subject Subject `json:"subject"` } -// swagger:model subject -// nolint:deadcode,unused -type stringEncodedSubject string - // swagger:parameters getExpand type SubjectSet struct { - // Namespace of the Relation Tuple + // Namespace of the Subject Set // - // in: query // required: true Namespace string `json:"namespace"` - // Object of the Relation Tuple + // Object of the Subject Set // - // in: query // required: true Object string `json:"object"` - // Relation of the Relation Tuple + // Relation of the Subject Set // - // in: query // required: true Relation string `json:"relation"` } @@ -133,8 +120,11 @@ type SubjectSet struct { var ( _, _ Subject = &SubjectID{}, &SubjectSet{} - ErrMalformedInput = herodot.ErrBadRequest.WithError("malformed string input") - ErrNilSubject = herodot.ErrBadRequest.WithError("subject is not allowed to be nil") + ErrMalformedInput = herodot.ErrBadRequest.WithError("malformed string input") + ErrNilSubject = herodot.ErrBadRequest.WithError("subject is not allowed to be nil") + ErrDuplicateSubject = herodot.ErrBadRequest.WithError("exactly one of subject_set or subject_id has to be provided") + ErrDroppedSubjectKey = herodot.ErrBadRequest.WithDebug(`provide "subject_id" or "subject_set.*"; support for "subject" was dropped`) + ErrIncompleteSubject = herodot.ErrBadRequest.WithError(`incomplete subject, provide "subject_id" or a complete "subject_set.*"`) ) // swagger:enum patchAction @@ -145,20 +135,6 @@ const ( ActionDelete patchAction = "delete" ) -// The patch request payload -// -// swagger:parameters patchRelationTuples -// nolint:deadcode,unused -type patchPayload struct { - // in:body - Payload []*PatchDelta -} - -type PatchDelta struct { - Action patchAction `json:"action"` - RelationTuple *InternalRelationTuple `json:"relation_tuple"` -} - func SubjectFromString(s string) (Subject, error) { if strings.Contains(s, "#") { return (&SubjectSet{}).FromString(s) @@ -236,6 +212,22 @@ func (s *SubjectSet) ToURLQuery() url.Values { } } +func (s *SubjectSet) SubjectID() *string { + return nil +} + +func (s *SubjectSet) SubjectSet() *SubjectSet { + return s +} + +func (s *SubjectID) SubjectID() *string { + return &s.ID +} + +func (s *SubjectID) SubjectSet() *SubjectSet { + return nil +} + // swagger:ignore func (s *SubjectID) ToProto() *acl.Subject { return &acl.Subject{ @@ -275,11 +267,7 @@ func (s *SubjectSet) Equals(v interface{}) bool { } func (s SubjectID) MarshalJSON() ([]byte, error) { - return []byte(`"` + s.String() + `"`), nil -} - -func (s SubjectSet) MarshalJSON() ([]byte, error) { - return []byte(`"` + s.String() + `"`), nil + return json.Marshal(s.ID) } func (r *InternalRelationTuple) String() string { @@ -326,30 +314,32 @@ func (r *InternalRelationTuple) DeriveSubject() *SubjectSet { } func (r *InternalRelationTuple) UnmarshalJSON(raw []byte) error { - subject := gjson.GetBytes(raw, "subject").Str - - var err error - r.Subject, err = SubjectFromString(subject) - if err != nil { - return err + var rq RelationQuery + if err := json.Unmarshal(raw, &rq); err != nil { + return errors.WithStack(err) + } + if rq.SubjectID != nil && rq.SubjectSet != nil { + return errors.WithStack(ErrDuplicateSubject) + } else if rq.SubjectID == nil && rq.SubjectSet == nil { + return errors.WithStack(ErrNilSubject) } - r.Namespace = gjson.GetBytes(raw, "namespace").Str - r.Object = gjson.GetBytes(raw, "object").Str - r.Relation = gjson.GetBytes(raw, "relation").Str + r.Namespace = rq.Namespace + r.Object = rq.Object + r.Relation = rq.Relation + + // validation was done before already + if rq.SubjectID == nil { + r.Subject = rq.SubjectSet + } else { + r.Subject = &SubjectID{ID: *rq.SubjectID} + } return nil } func (r *InternalRelationTuple) MarshalJSON() ([]byte, error) { - type t InternalRelationTuple - - enc, err := json.Marshal((*t)(r)) - if err != nil { - return nil, errors.WithStack(err) - } - - return sjson.SetBytes(enc, "subject", r.Subject.String()) + return json.Marshal(r.ToQuery()) } func (r *InternalRelationTuple) FromDataProvider(d TupleData) (*InternalRelationTuple, error) { @@ -375,32 +365,52 @@ func (r *InternalRelationTuple) ToProto() *acl.RelationTuple { } } +func (r *InternalRelationTuple) ToQuery() *RelationQuery { + return &RelationQuery{ + Namespace: r.Namespace, + Object: r.Object, + Relation: r.Relation, + SubjectID: r.Subject.SubjectID(), + SubjectSet: r.Subject.SubjectSet(), + } +} + func (r *InternalRelationTuple) FromURLQuery(query url.Values) (*InternalRelationTuple, error) { - if s := query.Get("subject"); s != "" { - var err error - r.Subject, err = SubjectFromString(s) - if err != nil { - return nil, err - } + q, err := (&RelationQuery{}).FromURLQuery(query) + if err != nil { + return nil, err } - r.Object = query.Get("object") - r.Relation = query.Get("relation") - r.Namespace = query.Get("namespace") + if s := q.Subject(); s == nil { + return nil, errors.WithStack(ErrNilSubject) + } else { + r.Subject = s + } + + r.Namespace = q.Namespace + r.Object = q.Object + r.Relation = q.Relation return r, nil } -func (r *InternalRelationTuple) ToURLQuery() url.Values { +func (r *InternalRelationTuple) ToURLQuery() (url.Values, error) { vals := url.Values{ "namespace": []string{r.Namespace}, "object": []string{r.Object}, "relation": []string{r.Relation}, } - if r.Subject != nil { - vals.Set("subject", r.Subject.String()) + switch s := r.Subject.(type) { + case *SubjectID: + vals.Set(subjectIDKey, s.ID) + case *SubjectSet: + vals.Set(subjectSetNamespaceKey, s.Namespace) + vals.Set(subjectSetObjectKey, s.Object) + vals.Set(subjectSetRelationKey, s.Relation) + case nil: + return nil, errors.WithStack(ErrNilSubject) } - return vals + return vals, nil } func (r *InternalRelationTuple) ToLoggerFields() logrus.Fields { @@ -408,31 +418,71 @@ func (r *InternalRelationTuple) ToLoggerFields() logrus.Fields { "namespace": r.Namespace, "object": r.Object, "relation": r.Relation, - "subject": r.Subject, + "subject": r.Subject.String(), } } func (q *RelationQuery) FromProto(query *acl.ListRelationTuplesRequest_Query) (*RelationQuery, error) { - r, err := (&InternalRelationTuple{}).FromDataProvider(query) - if err != nil { - return nil, err + q.Namespace = query.Namespace + q.Object = query.Object + q.Relation = query.Relation + // reset subject + q.SubjectID = nil + q.SubjectSet = nil + + if query.Subject != nil { + switch s := query.Subject.Ref.(type) { + case *acl.Subject_Id: + q.SubjectID = &s.Id + case *acl.Subject_Set: + q.SubjectSet = &SubjectSet{ + Namespace: s.Set.Namespace, + Object: s.Set.Object, + Relation: s.Set.Relation, + } + case nil: + return nil, errors.WithStack(ErrNilSubject) + } } - *q = RelationQuery(*r) return q, nil } +const ( + subjectIDKey = "subject_id" + subjectSetNamespaceKey = "subject_set.namespace" + subjectSetObjectKey = "subject_set.object" + subjectSetRelationKey = "subject_set.relation" +) + func (q *RelationQuery) FromURLQuery(query url.Values) (*RelationQuery, error) { if q == nil { q = &RelationQuery{} } - if s := query.Get("subject"); s != "" { - var err error - q.Subject, err = SubjectFromString(s) - if err != nil { - return nil, err + if query.Has("subject") { + return nil, errors.WithStack(ErrDroppedSubjectKey) + } + + // reset subject + q.SubjectID = nil + q.SubjectSet = nil + + switch { + case !query.Has(subjectIDKey) && !query.Has(subjectSetNamespaceKey) && !query.Has(subjectSetObjectKey) && !query.Has(subjectSetRelationKey): + // was not queried for the subject + case query.Has(subjectIDKey) && query.Has(subjectSetNamespaceKey) && query.Has(subjectSetObjectKey) && query.Has(subjectSetRelationKey): + return nil, errors.WithStack(ErrDuplicateSubject) + case query.Has(subjectIDKey): + q.SubjectID = pointerx.String(query.Get(subjectIDKey)) + case query.Has(subjectSetNamespaceKey) && query.Has(subjectSetObjectKey) && query.Has(subjectSetRelationKey): + q.SubjectSet = &SubjectSet{ + Namespace: query.Get(subjectSetNamespaceKey), + Object: query.Get(subjectSetObjectKey), + Relation: query.Get(subjectSetRelationKey), } + default: + return nil, errors.WithStack(ErrIncompleteSubject) } q.Object = query.Get("object") @@ -454,15 +504,31 @@ func (q *RelationQuery) ToURLQuery() url.Values { if q.Object != "" { v.Add("object", q.Object) } - if q.Subject != nil { - v.Add("subject", q.Subject.String()) + if q.SubjectID != nil { + v.Add(subjectIDKey, *q.SubjectID) + } else if q.SubjectSet != nil { + v.Add(subjectSetNamespaceKey, q.SubjectSet.Namespace) + v.Add(subjectSetObjectKey, q.SubjectSet.Object) + v.Add(subjectSetRelationKey, q.SubjectSet.Relation) } return v } +func (q *RelationQuery) Subject() Subject { + if q.SubjectID != nil { + return &SubjectID{ID: *q.SubjectID} + } else if q.SubjectSet != nil { + return q.SubjectSet + } + return nil +} + func (q *RelationQuery) String() string { - return fmt.Sprintf("namespace: %s; object: %s; relation: %s; subject: %s", q.Namespace, q.Object, q.Relation, q.Subject) + if q.SubjectID != nil { + return fmt.Sprintf("namespace: %s; object: %s; relation: %s; subject: %s", q.Namespace, q.Object, q.Relation, *q.SubjectID) + } + return fmt.Sprintf("namespace: %s; object: %s; relation: %s; subject: %v", q.Namespace, q.Object, q.Relation, q.SubjectSet) } func (r *InternalRelationTuple) Header() []string { @@ -470,7 +536,7 @@ func (r *InternalRelationTuple) Header() []string { "NAMESPACE", "OBJECT ID", "RELATION NAME", - "SUBJECT ID", + "SUBJECT", } } diff --git a/internal/relationtuple/definitions_test.go b/internal/relationtuple/definitions_test.go index 2e39a5917..e97efff16 100644 --- a/internal/relationtuple/definitions_test.go +++ b/internal/relationtuple/definitions_test.go @@ -8,6 +8,8 @@ import ( "strconv" "testing" + "github.com/ory/x/pointerx" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" @@ -236,7 +238,12 @@ func TestSubject(t *testing.T) { Object: "o", Relation: "r", }, - json: "\"n:o#r\"", + json: ` +{ + "namespace": "n", + "object": "o", + "relation": "r" +}`, }, { sub: &SubjectID{ID: "foo"}, @@ -246,7 +253,7 @@ func TestSubject(t *testing.T) { t.Run(fmt.Sprintf("case=%d", i), func(t *testing.T) { enc, err := json.Marshal(tc.sub) require.NoError(t, err) - assert.Equal(t, string(enc), tc.json) + assert.JSONEq(t, tc.json, string(enc)) }) } }) @@ -355,11 +362,15 @@ func TestInternalRelationTuple(t *testing.T) { Relation: "sr", }, }, - {}, + { + Subject: &SubjectID{}, + }, } { t.Run(fmt.Sprintf("case=%d", i), func(t *testing.T) { - res, err := (&InternalRelationTuple{}).FromURLQuery(r.ToURLQuery()) + vals, err := r.ToURLQuery() require.NoError(t, err) + res, err := (&InternalRelationTuple{}).FromURLQuery(vals) + require.NoError(t, err, "raw: %+v, enc: %+v", r, vals) assert.Equal(t, r, res) }) } @@ -368,22 +379,26 @@ func TestInternalRelationTuple(t *testing.T) { t.Run("case=url decoding-encoding", func(t *testing.T) { for i, v := range []url.Values{ { - "namespace": []string{"n"}, - "object": []string{"o"}, - "relation": []string{"r"}, - "subject": []string{"foo"}, + "namespace": []string{"n"}, + "object": []string{"o"}, + "relation": []string{"r"}, + "subject_id": []string{"foo"}, }, { - "namespace": []string{"n"}, - "object": []string{"o"}, - "relation": []string{"r"}, - "subject": []string{"sn:so#sr"}, + "namespace": []string{"n"}, + "object": []string{"o"}, + "relation": []string{"r"}, + "subject_set.namespace": []string{"sn"}, + "subject_set.object": []string{"so"}, + "subject_set.relation": []string{"sr"}, }, } { t.Run(fmt.Sprintf("case=%d", i), func(t *testing.T) { rt, err := (&InternalRelationTuple{}).FromURLQuery(v) require.NoError(t, err) - assert.Equal(t, v, rt.ToURLQuery()) + q, err := rt.ToURLQuery() + require.NoError(t, err) + assert.Equal(t, v, q) }) } }) @@ -457,6 +472,67 @@ func TestInternalRelationTuple(t *testing.T) { }) } }) + + t.Run("format=JSON", func(t *testing.T) { + t.Run("direction=encoding-decoding", func(t *testing.T) { + for _, tc := range []struct { + name string + rt *InternalRelationTuple + expected string + }{ + { + name: "with subject ID", + rt: &InternalRelationTuple{ + Namespace: "n", + Object: "o", + Relation: "r", + Subject: &SubjectID{ID: "s"}, + }, + expected: ` +{ + "namespace": "n", + "object": "o", + "relation": "r", + "subject_id": "s" +}`, + }, + { + name: "with subject set", + rt: &InternalRelationTuple{ + Namespace: "n", + Object: "o", + Relation: "r", + Subject: &SubjectSet{ + Namespace: "sn", + Object: "so", + Relation: "sr", + }, + }, + expected: ` +{ + "namespace": "n", + "object": "o", + "relation": "r", + "subject_set": { + "namespace": "sn", + "object": "so", + "relation": "sr" + } +}`, + }, + } { + t.Run("case="+tc.name, func(t *testing.T) { + raw, err := json.Marshal(tc.rt) + require.NoError(t, err) + assert.JSONEq(t, tc.expected, string(raw)) + + var dec InternalRelationTuple + require.NoError(t, json.Unmarshal(raw, &dec)) + assert.Equal(t, tc.rt, &dec) + }) + } + }) + }) } func TestRelationQuery(t *testing.T) { @@ -467,30 +543,32 @@ func TestRelationQuery(t *testing.T) { }{ { v: url.Values{ - "namespace": []string{"n"}, - "object": []string{"o"}, - "relation": []string{"r"}, - "subject": []string{"foo"}, + "namespace": []string{"n"}, + "object": []string{"o"}, + "relation": []string{"r"}, + "subject_id": []string{"foo"}, }, r: &RelationQuery{ Namespace: "n", Object: "o", Relation: "r", - Subject: &SubjectID{ID: "foo"}, + SubjectID: pointerx.String("foo"), }, }, { v: url.Values{ - "namespace": []string{"n"}, - "object": []string{"o"}, - "relation": []string{"r"}, - "subject": []string{"sn:so#sr"}, + "namespace": []string{"n"}, + "object": []string{"o"}, + "relation": []string{"r"}, + "subject_set.namespace": []string{"sn"}, + "subject_set.object": []string{"so"}, + "subject_set.relation": []string{"sr"}, }, r: &RelationQuery{ Namespace: "n", Object: "o", Relation: "r", - Subject: &SubjectSet{ + SubjectSet: &SubjectSet{ Namespace: "sn", Object: "so", Relation: "sr", diff --git a/internal/relationtuple/manager_requirements.go b/internal/relationtuple/manager_requirements.go index f9fcfdb02..568917112 100644 --- a/internal/relationtuple/manager_requirements.go +++ b/internal/relationtuple/manager_requirements.go @@ -7,6 +7,8 @@ import ( "strconv" "testing" + "github.com/ory/x/pointerx" + "github.com/ory/herodot" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -45,7 +47,7 @@ func ManagerTest(t *testing.T, m Manager, addNamespace func(context.Context, *te tupC := *tup t.Run(fmt.Sprintf("subject_type=%T", tupC.Subject), func(t *testing.T) { - resp, nextPage, err := m.GetRelationTuples(context.Background(), (*RelationQuery)(&tupC)) + resp, nextPage, err := m.GetRelationTuples(context.Background(), tupC.ToQuery()) require.NoError(t, err) assert.Equal(t, "", nextPage) assert.Equal(t, []*InternalRelationTuple{&tupC}, resp) @@ -129,7 +131,7 @@ func ManagerTest(t *testing.T, m Manager, addNamespace func(context.Context, *te { query: &RelationQuery{ Namespace: nspace, - Subject: &SubjectID{ID: "s 0"}, + SubjectID: pointerx.String("s 0"), }, expected: []*InternalRelationTuple{ tuples[0], @@ -139,7 +141,7 @@ func ManagerTest(t *testing.T, m Manager, addNamespace func(context.Context, *te query: &RelationQuery{ Namespace: nspace, Object: "o 0", - Subject: &SubjectID{ID: "s 0"}, + SubjectID: pointerx.String("s 0"), }, expected: []*InternalRelationTuple{ tuples[0], @@ -149,7 +151,7 @@ func ManagerTest(t *testing.T, m Manager, addNamespace func(context.Context, *te query: &RelationQuery{ Namespace: nspace, Relation: "r 0", - Subject: &SubjectID{ID: "s 0"}, + SubjectID: pointerx.String("s 0"), }, expected: []*InternalRelationTuple{ tuples[0], @@ -160,7 +162,7 @@ func ManagerTest(t *testing.T, m Manager, addNamespace func(context.Context, *te Namespace: nspace, Object: "o 0", Relation: "r 0", - Subject: &SubjectID{ID: "s 0"}, + SubjectID: pointerx.String("s 0"), }, expected: []*InternalRelationTuple{ tuples[0], @@ -284,13 +286,13 @@ func ManagerTest(t *testing.T, m Manager, addNamespace func(context.Context, *te t.Run(fmt.Sprintf("subject_type=%T", rt.Subject), func(t *testing.T) { require.NoError(t, m.WriteRelationTuples(context.Background(), rt)) - res, _, err := m.GetRelationTuples(context.Background(), (*RelationQuery)(rt)) + res, _, err := m.GetRelationTuples(context.Background(), rt.ToQuery()) require.NoError(t, err) assert.Equal(t, []*InternalRelationTuple{rt}, res) require.NoError(t, m.DeleteRelationTuples(context.Background(), rt)) - res, _, err = m.GetRelationTuples(context.Background(), (*RelationQuery)(rt)) + res, _, err = m.GetRelationTuples(context.Background(), rt.ToQuery()) require.NoError(t, err) assert.Len(t, res, 0) }) diff --git a/internal/relationtuple/read_server.go b/internal/relationtuple/read_server.go index 4ba4fe84d..cd3e9dbd7 100644 --- a/internal/relationtuple/read_server.go +++ b/internal/relationtuple/read_server.go @@ -23,19 +23,12 @@ func (h *handler) ListRelationTuples(ctx context.Context, req *acl.ListRelationT return nil, errors.New("invalid request") } - sub, err := SubjectFromProto(req.Query.Subject) + q, err := (&RelationQuery{}).FromProto(req.Query) if err != nil { - // this means we are not querying by subject - sub = nil + return nil, err } - rels, nextPage, err := h.d.RelationTupleManager().GetRelationTuples(ctx, - &RelationQuery{ - Namespace: req.Query.Namespace, - Object: req.Query.Object, - Relation: req.Query.Relation, - Subject: sub, - }, + rels, nextPage, err := h.d.RelationTupleManager().GetRelationTuples(ctx, q, x.WithSize(int(req.PageSize)), x.WithToken(req.PageToken), ) @@ -58,7 +51,7 @@ func (h *handler) ListRelationTuples(ctx context.Context, req *acl.ListRelationT // nolint:deadcode,unused type getRelationsParams struct { // swagger:allOf - RelationQuery + queryRelationTuple // swagger:allOf x.PaginationOptions } diff --git a/internal/relationtuple/swagger_definitions.go b/internal/relationtuple/swagger_definitions.go new file mode 100644 index 000000000..7c32e2a38 --- /dev/null +++ b/internal/relationtuple/swagger_definitions.go @@ -0,0 +1,43 @@ +package relationtuple + +// swagger:model InternalRelationTuple +// nolint:deadcode,unused +type relationTupleWithRequired struct { + // Namespace of the Relation Tuple + // + // required: true + Namespace string `json:"namespace"` + + // Object of the Relation Tuple + // + // required: true + Object string `json:"object"` + + // Relation of the Relation Tuple + // + // required: true + Relation string `json:"relation"` + + // SubjectID of the Relation Tuple + // + // Either SubjectSet or SubjectID are required. + SubjectID *string `json:"subject_id,omitempty"` + // SubjectSet of the Relation Tuple + // + // Either SubjectSet or SubjectID are required. + SubjectSet *SubjectSet `json:"subject_set,omitempty"` +} + +// The patch request payload +// +// swagger:parameters patchRelationTuples +// nolint:deadcode,unused +type patchPayload struct { + // in:body + Payload []*PatchDelta +} + +type PatchDelta struct { + Action patchAction `json:"action"` + RelationTuple *InternalRelationTuple `json:"relation_tuple"` +} diff --git a/internal/relationtuple/transact_server.go b/internal/relationtuple/transact_server.go index b9c30ee78..80afcb1b7 100644 --- a/internal/relationtuple/transact_server.go +++ b/internal/relationtuple/transact_server.go @@ -58,7 +58,55 @@ func (h *handler) TransactRelationTuples(ctx context.Context, req *acl.TransactR // nolint:deadcode,unused type bodyRelationTuple struct { // in: body - Payload InternalRelationTuple + Payload RelationQuery +} + +// The basic ACL relation tuple +// +// swagger:parameters getCheck deleteRelationTuple +// nolint:deadcode,unused +type queryRelationTuple struct { + // Namespace of the Relation Tuple + // + // in: query + // required: true + Namespace string `json:"namespace"` + + // Object of the Relation Tuple + // + // in: query + // required: true + Object string `json:"object"` + + // Relation of the Relation Tuple + // + // in: query + // required: true + Relation string `json:"relation"` + + // SubjectID of the Relation Tuple + // + // in: query + // Either subject_set.* or subject_id are required. + SubjectID string `json:"subject_id"` + + // Namespace of the Subject Set + // + // in: query + // Either subject_set.* or subject_id are required. + SNamespace string `json:"subject_set.namespace"` + + // Object of the Subject Set + // + // in: query + // Either subject_set.* or subject_id are required. + SObject string `json:"subject_set.object"` + + // Relation of the Subject Set + // + // in: query + // Either subject_set.* or subject_id are required. + SRelation string `json:"subject_set.relation"` } // swagger:route PUT /relation-tuples write createRelationTuple @@ -76,7 +124,7 @@ type bodyRelationTuple struct { // Schemes: http, https // // Responses: -// 201: InternalRelationTuple +// 201: RelationQuery // 400: genericError // 500: genericError func (h *handler) createRelation(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { @@ -95,7 +143,13 @@ func (h *handler) createRelation(w http.ResponseWriter, r *http.Request, _ httpr return } - h.d.Writer().WriteCreated(w, r, RouteBase+"?"+rel.ToURLQuery().Encode(), rel) + q, err := rel.ToURLQuery() + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + h.d.Writer().WriteCreated(w, r, RouteBase+"?"+q.Encode(), &rel) } // swagger:route DELETE /relation-tuples write deleteRelationTuple diff --git a/internal/relationtuple/transact_server_test.go b/internal/relationtuple/transact_server_test.go index 113e6cfb5..e401dae65 100644 --- a/internal/relationtuple/transact_server_test.go +++ b/internal/relationtuple/transact_server_test.go @@ -80,7 +80,7 @@ func TestWriteHandlers(t *testing.T) { t.Run("check=is contained in the manager", func(t *testing.T) { // set a size > 1 just to make sure it gets all - actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(context.Background(), (*relationtuple.RelationQuery)(rt), x.WithSize(10)) + actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(context.Background(), rt.ToQuery(), x.WithSize(10)) require.NoError(t, err) assert.Equal(t, []*relationtuple.InternalRelationTuple{rt}, actualRTs) }) @@ -101,7 +101,7 @@ func TestWriteHandlers(t *testing.T) { assert.Equal(t, http.StatusBadRequest, resp.StatusCode) }) - t.Run("case=special chars error on creation already", func(t *testing.T) { + t.Run("case=special chars", func(t *testing.T) { nspace := addNamespace(t) rts := []*relationtuple.InternalRelationTuple{ @@ -119,7 +119,7 @@ func TestWriteHandlers(t *testing.T) { Namespace: nspace.Name, Object: "@all", Relation: "member", - Subject: &relationtuple.SubjectID{ID: "this:will#be interpreted:as a@subject set"}, + Subject: &relationtuple.SubjectID{ID: "this:could#be interpreted:as a@subject set"}, }, } @@ -128,8 +128,7 @@ func TestWriteHandlers(t *testing.T) { require.NoError(t, err) resp := doCreate(payload) - assert.GreaterOrEqual(t, resp.StatusCode, http.StatusBadRequest) - assert.Less(t, resp.StatusCode, http.StatusInternalServerError) + assert.Equal(t, http.StatusCreated, resp.StatusCode) } actual, next, err := reg.RelationTupleManager().GetRelationTuples(context.Background(), &relationtuple.RelationQuery{ @@ -137,7 +136,10 @@ func TestWriteHandlers(t *testing.T) { }) require.NoError(t, err) assert.Equal(t, "", next) - assert.Len(t, actual, 0) + assert.Len(t, actual, 2) + for _, rt := range rts { + assert.Contains(t, actual, rt) + } }) }) @@ -153,14 +155,16 @@ func TestWriteHandlers(t *testing.T) { } require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), rt)) - req, err := http.NewRequest(http.MethodDelete, ts.URL+relationtuple.RouteBase+"?"+rt.ToURLQuery().Encode(), nil) + q, err := rt.ToURLQuery() + require.NoError(t, err) + req, err := http.NewRequest(http.MethodDelete, ts.URL+relationtuple.RouteBase+"?"+q.Encode(), nil) require.NoError(t, err) resp, err := ts.Client().Do(req) require.NoError(t, err) assert.Equal(t, http.StatusNoContent, resp.StatusCode) // set a size > 1 just to make sure it gets all - actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(context.Background(), (*relationtuple.RelationQuery)(rt), x.WithSize(10)) + actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(context.Background(), rt.ToQuery(), x.WithSize(10)) require.NoError(t, err) assert.Equal(t, []*relationtuple.InternalRelationTuple{}, actualRTs) }) @@ -241,7 +245,7 @@ func TestWriteHandlers(t *testing.T) { assert.Equal(t, http.StatusNotFound, resp.StatusCode) // set a size > 1 just to make sure it gets all - actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(context.Background(), (*relationtuple.RelationQuery)(deltas[0].RelationTuple), x.WithSize(10)) + actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(context.Background(), deltas[0].RelationTuple.ToQuery(), x.WithSize(10)) require.NoError(t, err) assert.Len(t, actualRTs, 0) }) @@ -338,7 +342,7 @@ func TestWriteHandlers(t *testing.T) { "namespace":"role", "object":"super-admin", "relation":"member", - "subject":"role:company-admin" + "subject_id":"role:company-admin" } } ]` diff --git a/spec/api.json b/spec/api.json index b47be3bd9..dd51daf30 100755 --- a/spec/api.json +++ b/spec/api.json @@ -68,8 +68,26 @@ }, { "type": "string", - "description": "Subject of the Relation Tuple\n\nThe subject follows the subject string encoding format.", - "name": "subject", + "description": "SubjectID of the Relation Tuple", + "name": "subject_id", + "in": "query" + }, + { + "type": "string", + "description": "Namespace of the Subject Set", + "name": "subject_set.namespace", + "in": "query" + }, + { + "type": "string", + "description": "Object of the Subject Set", + "name": "subject_set.object", + "in": "query" + }, + { + "type": "string", + "description": "Relation of the Subject Set", + "name": "subject_set.relation", "in": "query" } ], @@ -172,7 +190,7 @@ "name": "Payload", "in": "body", "schema": { - "$ref": "#/definitions/InternalRelationTuple" + "$ref": "#/definitions/RelationQuery" } } ], @@ -275,21 +293,21 @@ "parameters": [ { "type": "string", - "description": "Namespace of the Relation Tuple", + "description": "Namespace of the Subject Set", "name": "namespace", "in": "query", "required": true }, { "type": "string", - "description": "Object of the Relation Tuple", + "description": "Object of the Subject Set", "name": "object", "in": "query", "required": true }, { "type": "string", - "description": "Relation of the Relation Tuple", + "description": "Relation of the Subject Set", "name": "relation", "in": "query", "required": true @@ -504,23 +522,47 @@ "parameters": [ { "type": "string", + "description": "Namespace of the Relation Tuple", "name": "namespace", "in": "query", "required": true }, { "type": "string", + "description": "Object of the Relation Tuple", "name": "object", - "in": "query" + "in": "query", + "required": true }, { "type": "string", + "description": "Relation of the Relation Tuple", "name": "relation", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "SubjectID of the Relation Tuple", + "name": "subject_id", "in": "query" }, { "type": "string", - "name": "subject", + "description": "Namespace of the Subject Set", + "name": "subject_set.namespace", + "in": "query" + }, + { + "type": "string", + "description": "Object of the Subject Set", + "name": "subject_set.object", + "in": "query" + }, + { + "type": "string", + "description": "Relation of the Subject Set", + "name": "subject_set.relation", "in": "query" }, { @@ -628,15 +670,15 @@ "name": "Payload", "in": "body", "schema": { - "$ref": "#/definitions/InternalRelationTuple" + "$ref": "#/definitions/RelationQuery" } } ], "responses": { "201": { - "description": "InternalRelationTuple", + "description": "RelationQuery", "schema": { - "$ref": "#/definitions/InternalRelationTuple" + "$ref": "#/definitions/RelationQuery" } }, "400": { @@ -744,8 +786,26 @@ }, { "type": "string", - "description": "Subject of the Relation Tuple\n\nThe subject follows the subject string encoding format.", - "name": "subject", + "description": "SubjectID of the Relation Tuple", + "name": "subject_id", + "in": "query" + }, + { + "type": "string", + "description": "Namespace of the Subject Set", + "name": "subject_set.namespace", + "in": "query" + }, + { + "type": "string", + "description": "Object of the Subject Set", + "name": "subject_set.object", + "in": "query" + }, + { + "type": "string", + "description": "Relation of the Subject Set", + "name": "subject_set.relation", "in": "query" } ], @@ -974,24 +1034,27 @@ "required": [ "namespace", "object", - "relation", - "subject" + "relation" ], "properties": { "namespace": { - "description": "Namespace of the Relation Tuple\n\nin: query", + "description": "Namespace of the Relation Tuple", "type": "string" }, "object": { - "description": "Object of the Relation Tuple\n\nin: query", + "description": "Object of the Relation Tuple", "type": "string" }, "relation": { - "description": "Relation of the Relation Tuple\n\nin: query", + "description": "Relation of the Relation Tuple", "type": "string" }, - "subject": { - "$ref": "#/definitions/subject" + "subject_id": { + "description": "SubjectID of the Relation Tuple\n\nEither SubjectSet or SubjectID are required.", + "type": "string" + }, + "subject_set": { + "$ref": "#/definitions/SubjectSet" } } }, @@ -1010,11 +1073,59 @@ } } }, + "RelationQuery": { + "type": "object", + "required": [ + "namespace" + ], + "properties": { + "namespace": { + "description": "Namespace of the Relation Tuple", + "type": "string" + }, + "object": { + "description": "Object of the Relation Tuple", + "type": "string" + }, + "relation": { + "description": "Relation of the Relation Tuple", + "type": "string" + }, + "subject_id": { + "description": "SubjectID of the Relation Tuple\n\nEither SubjectSet or SubjectID can be provided.", + "type": "string" + }, + "subject_set": { + "$ref": "#/definitions/SubjectSet" + } + } + }, + "SubjectSet": { + "type": "object", + "required": [ + "namespace", + "object", + "relation" + ], + "properties": { + "namespace": { + "description": "Namespace of the Subject Set", + "type": "string" + }, + "object": { + "description": "Object of the Subject Set", + "type": "string" + }, + "relation": { + "description": "Relation of the Subject Set", + "type": "string" + } + } + }, "expandTree": { "type": "object", "required": [ - "type", - "subject" + "type" ], "properties": { "children": { @@ -1023,8 +1134,11 @@ "$ref": "#/definitions/expandTree" } }, - "subject": { - "$ref": "#/definitions/subject" + "subject_id": { + "type": "string" + }, + "subject_set": { + "$ref": "#/definitions/SubjectSet" }, "type": { "type": "string", @@ -1087,9 +1201,6 @@ } } }, - "subject": { - "type": "string" - }, "version": { "type": "object", "properties": { diff --git a/spec/swagger.json b/spec/swagger.json new file mode 100755 index 000000000..6753b1843 --- /dev/null +++ b/spec/swagger.json @@ -0,0 +1,1215 @@ +{ + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "swagger": "2.0", + "info": { + "description": "Ory Keto is a cloud native access control server providing best-practice patterns (RBAC, ABAC, ACL, AWS IAM Policies, Kubernetes Roles, ...) via REST APIs.", + "title": "ORY Keto", + "contact": { + "name": "ORY", + "url": "https://www.ory.sh", + "email": "hi@ory.sh" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/ory/keto/blob/master/LICENSE" + }, + "version": "Latest" + }, + "basePath": "/", + "paths": { + "/check": { + "get": { + "description": "To learn how relation tuples and the check works, head over to [the documentation](../concepts/relation-tuples.mdx).", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "read" + ], + "summary": "Check a relation tuple", + "operationId": "getCheck", + "parameters": [ + { + "type": "string", + "description": "Namespace of the Relation Tuple", + "name": "namespace", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Object of the Relation Tuple", + "name": "object", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Relation of the Relation Tuple", + "name": "relation", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "SubjectID of the Relation Tuple", + "name": "subject_id", + "in": "query" + }, + { + "type": "string", + "description": "Namespace of the Subject Set", + "name": "subject_set.namespace", + "in": "query" + }, + { + "type": "string", + "description": "Object of the Subject Set", + "name": "subject_set.object", + "in": "query" + }, + { + "type": "string", + "description": "Relation of the Subject Set", + "name": "subject_set.relation", + "in": "query" + } + ], + "responses": { + "200": { + "description": "getCheckResponse", + "schema": { + "$ref": "#/definitions/getCheckResponse" + } + }, + "400": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + }, + "403": { + "description": "getCheckResponse", + "schema": { + "$ref": "#/definitions/getCheckResponse" + } + }, + "500": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + } + } + }, + "post": { + "description": "To learn how relation tuples and the check works, head over to [the documentation](../concepts/relation-tuples.mdx).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "read" + ], + "summary": "Check a relation tuple", + "operationId": "postCheck", + "parameters": [ + { + "name": "Payload", + "in": "body", + "schema": { + "$ref": "#/definitions/RelationQuery" + } + } + ], + "responses": { + "200": { + "description": "getCheckResponse", + "schema": { + "$ref": "#/definitions/getCheckResponse" + } + }, + "400": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + }, + "403": { + "description": "getCheckResponse", + "schema": { + "$ref": "#/definitions/getCheckResponse" + } + }, + "500": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + } + } + } + }, + "/expand": { + "get": { + "description": "Use this endpoint to expand a relation tuple.", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "read" + ], + "summary": "Expand a Relation Tuple", + "operationId": "getExpand", + "parameters": [ + { + "type": "string", + "description": "Namespace of the Subject Set", + "name": "namespace", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Object of the Subject Set", + "name": "object", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Relation of the Subject Set", + "name": "relation", + "in": "query", + "required": true + }, + { + "type": "integer", + "format": "int64", + "name": "max-depth", + "in": "query" + } + ], + "responses": { + "200": { + "description": "expandTree", + "schema": { + "$ref": "#/definitions/expandTree" + } + }, + "400": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + }, + "404": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + }, + "500": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + } + } + } + }, + "/health/alive": { + "get": { + "description": "This endpoint returns a 200 status code when the HTTP server is up running.\nThis status does currently not include checks whether the database connection is working.\n\nIf the service supports TLS Edge Termination, this endpoint does not require the\n`X-Forwarded-Proto` header to be set.\n\nBe aware that if you are running multiple nodes of this service, the health status will never\nrefer to the cluster state, only to a single instance.", + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Check alive status", + "operationId": "isInstanceAlive", + "responses": { + "200": { + "description": "healthStatus", + "schema": { + "$ref": "#/definitions/healthStatus" + } + }, + "500": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + } + } + } + }, + "/health/ready": { + "get": { + "description": "This endpoint returns a 200 status code when the HTTP server is up running and the environment dependencies (e.g.\nthe database) are responsive as well.\n\nIf the service supports TLS Edge Termination, this endpoint does not require the\n`X-Forwarded-Proto` header to be set.\n\nBe aware that if you are running multiple nodes of this service, the health status will never\nrefer to the cluster state, only to a single instance.", + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Check readiness status", + "operationId": "isInstanceReady", + "responses": { + "200": { + "description": "healthStatus", + "schema": { + "$ref": "#/definitions/healthStatus" + } + }, + "503": { + "description": "healthNotReadyStatus", + "schema": { + "$ref": "#/definitions/healthNotReadyStatus" + } + } + } + } + }, + "/relation-tuples": { + "get": { + "description": "Get all relation tuples that match the query. Only the namespace field is required.", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "read" + ], + "summary": "Query relation tuples", + "operationId": "getRelationTuples", + "parameters": [ + { + "type": "string", + "description": "Namespace of the Relation Tuple", + "name": "namespace", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Object of the Relation Tuple", + "name": "object", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Relation of the Relation Tuple", + "name": "relation", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "SubjectID of the Relation Tuple", + "name": "subject_id", + "in": "query" + }, + { + "type": "string", + "description": "Namespace of the Subject Set", + "name": "subject_set.namespace", + "in": "query" + }, + { + "type": "string", + "description": "Object of the Subject Set", + "name": "subject_set.object", + "in": "query" + }, + { + "type": "string", + "description": "Relation of the Subject Set", + "name": "subject_set.relation", + "in": "query" + }, + { + "type": "string", + "name": "page_token", + "in": "query" + }, + { + "type": "integer", + "format": "int64", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "getRelationTuplesResponse", + "schema": { + "$ref": "#/definitions/getRelationTuplesResponse" + } + }, + "404": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + }, + "500": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + } + } + }, + "put": { + "description": "Use this endpoint to create a relation tuple.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "write" + ], + "summary": "Create a Relation Tuple", + "operationId": "createRelationTuple", + "parameters": [ + { + "name": "Payload", + "in": "body", + "schema": { + "$ref": "#/definitions/RelationQuery" + } + } + ], + "responses": { + "201": { + "description": "RelationQuery", + "schema": { + "$ref": "#/definitions/RelationQuery" + } + }, + "400": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + }, + "500": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + } + } + }, + "delete": { + "description": "Use this endpoint to delete a relation tuple.", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "write" + ], + "summary": "Delete a Relation Tuple", + "operationId": "deleteRelationTuple", + "parameters": [ + { + "type": "string", + "description": "Namespace of the Relation Tuple", + "name": "namespace", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Object of the Relation Tuple", + "name": "object", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Relation of the Relation Tuple", + "name": "relation", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "SubjectID of the Relation Tuple", + "name": "subject_id", + "in": "query" + }, + { + "type": "string", + "description": "Namespace of the Subject Set", + "name": "subject_set.namespace", + "in": "query" + }, + { + "type": "string", + "description": "Object of the Subject Set", + "name": "subject_set.object", + "in": "query" + }, + { + "type": "string", + "description": "Relation of the Subject Set", + "name": "subject_set.relation", + "in": "query" + } + ], + "responses": { + "204": { + "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 201." + }, + "400": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + }, + "500": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + } + } + }, + "patch": { + "description": "Use this endpoint to patch one or more relation tuples.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "write" + ], + "summary": "Patch Multiple Relation Tuples", + "operationId": "patchRelationTuples", + "parameters": [ + { + "name": "Payload", + "in": "body", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/PatchDelta" + } + } + } + ], + "responses": { + "204": { + "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 201." + }, + "400": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + }, + "404": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + }, + "500": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + } + } + } + }, + "/version": { + "get": { + "description": "This endpoint returns the service version typically notated using semantic versioning.\n\nIf the service supports TLS Edge Termination, this endpoint does not require the\n`X-Forwarded-Proto` header to be set.\n\nBe aware that if you are running multiple nodes of this service, the health status will never\nrefer to the cluster state, only to a single instance.", + "produces": [ + "application/json" + ], + "tags": [ + "version" + ], + "summary": "Get service version", + "operationId": "getVersion", + "responses": { + "200": { + "description": "version", + "schema": { + "$ref": "#/definitions/version" + } + } + } + } + } + }, + "definitions": { + "InternalRelationTuple": { + "type": "object", + "required": [ + "namespace", + "object", + "relation" + ], + "properties": { + "namespace": { + "description": "Namespace of the Relation Tuple", + "type": "string" + }, + "object": { + "description": "Object of the Relation Tuple", + "type": "string" + }, + "relation": { + "description": "Relation of the Relation Tuple", + "type": "string" + }, + "subject_id": { + "description": "SubjectID of the Relation Tuple\n\nEither SubjectSet or SubjectID are required.", + "type": "string" + }, + "subject_set": { + "$ref": "#/definitions/SubjectSet" + } + } + }, + "PatchDelta": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "insert", + "delete" + ] + }, + "relation_tuple": { + "$ref": "#/definitions/InternalRelationTuple" + } + } + }, + "RelationQuery": { + "type": "object", + "required": [ + "namespace" + ], + "properties": { + "namespace": { + "description": "Namespace of the Relation Tuple", + "type": "string" + }, + "object": { + "description": "Object of the Relation Tuple", + "type": "string" + }, + "relation": { + "description": "Relation of the Relation Tuple", + "type": "string" + }, + "subject_id": { + "description": "SubjectID of the Relation Tuple\n\nEither SubjectSet or SubjectID can be provided.", + "type": "string" + }, + "subject_set": { + "$ref": "#/definitions/SubjectSet" + } + } + }, + "SubjectSet": { + "type": "object", + "required": [ + "namespace", + "object", + "relation" + ], + "properties": { + "namespace": { + "description": "Namespace of the Subject Set", + "type": "string" + }, + "object": { + "description": "Object of the Subject Set", + "type": "string" + }, + "relation": { + "description": "Relation of the Subject Set", + "type": "string" + } + } + }, + "expandTree": { + "type": "object", + "required": [ + "type", + "subject" + ], + "properties": { + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/expandTree" + } + }, + "subject": { + "$ref": "#/definitions/subject" + }, + "type": { + "type": "string", + "enum": [ + "union", + "exclusion", + "intersection", + "leaf" + ] + } + } + }, + "getCheckResponse": { + "description": "The content of the allowed field is mirrored in the HTTP status code.", + "type": "object", + "title": "Represents the response for a check request.", + "required": [ + "allowed" + ], + "properties": { + "allowed": { + "description": "whether the relation tuple is allowed", + "type": "boolean" + } + } + }, + "getRelationTuplesResponse": { + "type": "object", + "properties": { + "next_page_token": { + "description": "The opaque token to provide in a subsequent request\nto get the next page. It is the empty string iff this is\nthe last page.", + "type": "string" + }, + "relation_tuples": { + "type": "array", + "items": { + "$ref": "#/definitions/InternalRelationTuple" + } + } + } + }, + "healthNotReadyStatus": { + "type": "object", + "properties": { + "errors": { + "description": "Errors contains a list of errors that caused the not ready status.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "healthStatus": { + "type": "object", + "properties": { + "status": { + "description": "Status always contains \"ok\".", + "type": "string" + } + } + }, + "subject": { + "type": "object" + }, + "version": { + "type": "object", + "properties": { + "version": { + "description": "Version is the service's version.", + "type": "string" + } + } + } + } +} \ No newline at end of file