CVE-2022-39341: Other (NVD-CWE-Other)
value: OpenFGA is an authorization/permission engine. Versions prior to version 0.2.4 are vulnerable to authorization bypass under certain conditions. Users who have wildcard (`*`) defined on tupleset relations in their authorization model are vulnerable. Version 0.2.4 contains a patch for this issue.
CWE description: NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
CVSS v3 base score: 9.8
Published: 2022-10-25T17:15Z
Repository: https://github.com/openfga/openfga
Language: Go
Files changed: server/commands/check.go, server/commands/check_utils.go, server/commands/expand.go, server/test/check.go, server/test/expand.go
Commit date: 2022-10-20 16:29:12 -0600
Commit message: Merge pull request from GHSA-vj4m-83m8-xpw5

* fix: return error if wildcard is encountered in ttu evaluation

* chore: address one of maria's comments

* chore: fix public facing error message and test name

* fix: return error if '*' encountered in tupleset relation in Expand
Diff: diff --git a/server/commands/check.go b/server/commands/check.go
index c51c2f1..b07ad8f 100644
--- a/server/commands/check.go
+++ b/server/commands/check.go
@@ -1,9 +1,10 @@
 package commands
 
 import (
 	"context"
+	"fmt"
 	"sync"
 
 	"github.com/go-errors/errors"
 	"github.com/openfga/openfga/pkg/logger"
 	tupleUtils "github.com/openfga/openfga/pkg/tuple"
@@ -19,11 +20,11 @@ import (
 	"go.opentelemetry.io/otel/trace"
 	"go.uber.org/zap"
 	"golang.org/x/sync/errgroup"
 )
 
-const AllUsers = "*"
+const Wildcard = "*"
 
 // A CheckQuery can be used to Check if a User has a Relation to an Object
 // CheckQuery instances may be safely shared by multiple go-routines
 type CheckQuery struct {
 	logger           logger.Logger
@@ -399,11 +400,13 @@ func (query *CheckQuery) resolveDifference(ctx context.Context, rc *resolutionCo
 func (query *CheckQuery) resolveTupleToUserset(ctx context.Context, rc *resolutionContext, node *openfgapb.Userset_TupleToUserset) error {
 	relation := node.TupleToUserset.GetTupleset().GetRelation()
 	if relation == "" {
 		relation = rc.tk.GetRelation()
 	}
-	findTK := &openfgapb.TupleKey{Object: rc.tk.GetObject(), Relation: relation}
+
+	findTK := tupleUtils.NewTupleKey(rc.tk.GetObject(), relation, "")
+
 	tracer := rc.tracer.AppendTupleToUserset().AppendString(tupleUtils.ToObjectRelationString(findTK.GetObject(), relation))
 	iter, err := rc.read(ctx, query.datastore, findTK)
 	if err != nil {
 		return serverErrors.HandleError("", err)
 	}
@@ -427,10 +430,27 @@ func (query *CheckQuery) resolveTupleToUserset(ctx context.Context, rc *resoluti
 			break // the user was resolved already, avoid launching extra lookups
 		}
 
 		userObj, userRel := tupleUtils.SplitObjectRelation(tuple.GetUser())
 
+		if userObj == Wildcard {
+			objectType, _ := tupleUtils.SplitObject(rc.tk.GetObject())
+
+			query.logger.WarnWithContext(
+				ctx,
+				fmt.Sprintf("unexpected wildcard evaluated on tupleset relation '%s'", relation),
+				zap.String("store_id", rc.store),
+				zap.String("authorization_model_id", rc.modelID),
+				zap.String("object_type", objectType),
+			)
+
+			return serverErrors.InvalidTuple(
+				fmt.Sprintf("unexpected wildcard evaluated on relation '%s#%s'", objectType, relation),
+				tupleUtils.NewTupleKey(rc.tk.GetObject(), relation, Wildcard),
+			)
+		}
+
 		if !tupleUtils.IsValidObject(userObj) {
 			continue // TupleToUserset tuplesets should be of the form 'objectType:id' or 'objectType:id#relation' but are not guaranteed to be because it is neither a user or userset
 		}
 
 		usersetRel := node.TupleToUserset.GetComputedUserset().GetRelation()
diff --git a/server/commands/check_utils.go b/server/commands/check_utils.go
index 0d969d1..0863ec3 100644
--- a/server/commands/check_utils.go
+++ b/server/commands/check_utils.go
@@ -193,11 +193,11 @@ func (u *userSet) Get(value string) (resolutionTracer, bool) {
 	defer u.m.Unlock()
 
 	var found bool
 	var rt resolutionTracer
 	if rt, found = u.u[value]; !found {
-		if rt, found = u.u[AllUsers]; !found {
+		if rt, found = u.u[Wildcard]; !found {
 			return nil, false
 		}
 	}
 	return rt, found
 }
diff --git a/server/commands/expand.go b/server/commands/expand.go
index 018b4a1..5503257 100644
--- a/server/commands/expand.go
+++ b/server/commands/expand.go
@@ -1,18 +1,20 @@
 package commands
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/openfga/openfga/pkg/logger"
 	tupleUtils "github.com/openfga/openfga/pkg/tuple"
 	"github.com/openfga/openfga/pkg/utils"
 	serverErrors "github.com/openfga/openfga/server/errors"
 	"github.com/openfga/openfga/server/validation"
 	"github.com/openfga/openfga/storage"
 	openfgapb "go.buf.build/openfga/go/openfga/api/openfga/v1"
 	"go.opentelemetry.io/otel/trace"
+	"go.uber.org/zap"
 	"golang.org/x/sync/errgroup"
 )
 
 // ExpandQuery resolves a target TupleKey into a UsersetTree by expanding type definitions.
 type ExpandQuery struct {
@@ -56,21 +58,27 @@ func (query *ExpandQuery) Execute(ctx context.Context, req *openfgapb.ExpandRequ
 			Root: root,
 		},
 	}, nil
 }
 
-func (query *ExpandQuery) resolveUserset(ctx context.Context, store, modelID string, userset *openfgapb.Userset, tk *openfgapb.TupleKey, metadata *utils.ResolutionMetadata) (*openfgapb.UsersetTree_Node, error) {
+func (query *ExpandQuery) resolveUserset(
+	ctx context.Context,
+	store, modelID string,
+	userset *openfgapb.Userset,
+	tk *openfgapb.TupleKey,
+	metadata *utils.ResolutionMetadata,
+) (*openfgapb.UsersetTree_Node, error) {
 	ctx, span := query.tracer.Start(ctx, "resolveUserset")
 	defer span.End()
 
 	switch us := userset.Userset.(type) {
 	case nil, *openfgapb.Userset_This:
 		return query.resolveThis(ctx, store, tk, metadata)
 	case *openfgapb.Userset_ComputedUserset:
 		return query.resolveComputedUserset(ctx, us.ComputedUserset, tk, metadata)
 	case *openfgapb.Userset_TupleToUserset:
-		return query.resolveTupleToUserset(ctx, store, us.TupleToUserset, tk, metadata)
+		return query.resolveTupleToUserset(ctx, store, modelID, us.TupleToUserset, tk, metadata)
 	case *openfgapb.Userset_Union:
 		return query.resolveUnionUserset(ctx, store, modelID, us.Union, tk, metadata)
 	case *openfgapb.Userset_Difference:
 		return query.resolveDifferenceUserset(ctx, store, modelID, us.Difference, tk, metadata)
 	case *openfgapb.Userset_Intersection:
@@ -150,28 +158,39 @@ func (query *ExpandQuery) resolveComputedUserset(ctx context.Context, userset *o
 		},
 	}, nil
 }
 
 // resolveTupleToUserset creates a new leaf node containing the result of expanding a TupleToUserset rewrite.
-func (query *ExpandQuery) resolveTupleToUserset(ctx context.Context, store string, userset *openfgapb.TupleToUserset, tk *openfgapb.TupleKey, metadata *utils.ResolutionMetadata) (*openfgapb.UsersetTree_Node, error) {
+func (query *ExpandQuery) resolveTupleToUserset(
+	ctx context.Context,
+	store, modelID string,
+	userset *openfgapb.TupleToUserset,
+	tk *openfgapb.TupleKey,
+	metadata *utils.ResolutionMetadata,
+) (*openfgapb.UsersetTree_Node, error) {
 	ctx, span := query.tracer.Start(ctx, "resolveTupleToUserset")
 	defer span.End()
 
+	targetObject := tk.GetObject()
+
+	tupleset := userset.GetTupleset().GetRelation()
 	tsKey := &openfgapb.TupleKey{
-		Object:   tk.GetObject(),
-		Relation: userset.GetTupleset().GetRelation(),
+		Object:   targetObject,
+		Relation: tupleset,
 	}
+
 	if tsKey.GetRelation() == "" {
 		tsKey.Relation = tk.GetRelation()
 	}
 
 	metadata.AddReadCall()
 	iter, err := query.datastore.Read(ctx, store, tsKey)
 	if err != nil {
 		return nil, serverErrors.HandleError("", err)
 	}
 	defer iter.Stop()
+
 	var computed []*openfgapb.UsersetTree_Computed
 	seen := make(map[string]bool)
 	for {
 		tuple, err := iter.Next()
 		if err != nil {
@@ -179,33 +198,56 @@ func (query *ExpandQuery) resolveTupleToUserset(ctx context.Context, store strin
 				break
 			}
 			return nil, serverErrors.HandleError("", err)
 		}
 		user := tuple.GetKey().GetUser()
+
+		if user == Wildcard {
+			objectType, _ := tupleUtils.SplitObject(targetObject)
+
+			query.logger.WarnWithContext(
+				ctx,
+				fmt.Sprintf("unexpected wildcard evaluated on tupleset relation '%s'", tupleset),
+				zap.String("store_id", store),
+				zap.String("authorization_model_id", modelID),
+				zap.String("object_type", objectType),
+			)
+
+			return nil, serverErrors.InvalidTuple(
+				fmt.Sprintf("unexpected wildcard evaluated on relation '%s#%s'", objectType, tupleset),
+				tupleUtils.NewTupleKey(targetObject, tupleset, Wildcard),
+			)
+		}
+
 		// user must contain a type (i.e., be an object or userset)
 		if tupleUtils.GetType(user) == "" {
 			continue
 		}
+
 		tObject, tRelation := tupleUtils.SplitObjectRelation(user)
 		// We only proceed in the case that tRelation == userset.GetComputedUserset().GetRelation().
 		// tRelation may be empty, and in this case, we set it to userset.GetComputedUserset().GetRelation().
 		if tRelation == "" {
 			tRelation = userset.GetComputedUserset().GetRelation()
 		}
+
 		if tRelation != userset.GetComputedUserset().GetRelation() {
 			continue
 		}
+
 		cs := &openfgapb.TupleKey{
 			Object:   tObject,
 			Relation: tRelation,
 		}
+
 		computedRelation := toObjectRelation(cs)
 		if !seen[computedRelation] {
 			computed = append(computed, &openfgapb.UsersetTree_Computed{Userset: computedRelation})
 			seen[computedRelation] = true
 		}
 	}
+
 	return &openfgapb.UsersetTree_Node{
 		Name: toObjectRelation(tk),
 		Value: &openfgapb.UsersetTree_Node_Leaf{
 			Leaf: &openfgapb.UsersetTree_Leaf{
 				Value: &openfgapb.UsersetTree_Leaf_TupleToUserset{
diff --git a/server/test/check.go b/server/test/check.go
index e031838..dc58fdf 100644
--- a/server/test/check.go
+++ b/server/test/check.go
@@ -1165,10 +1165,108 @@ func TestCheckQuery(t *testing.T, datastore storage.OpenFGADatastore) {
 			},
 			response: &openfgapb.CheckResponse{
 				Allowed: true,
 			},
 		},
+		{
+			name:             "Error if * encountered in TupleToUserset evaluation",
+			resolveNodeLimit: defaultResolveNodeLimit,
+			request: &openfgapb.CheckRequest{
+				TupleKey: tuple.NewTupleKey("document:doc1", "viewer", "user:anne"),
+			},
+			typeDefinitions: []*openfgapb.TypeDefinition{
+				{
+					Type: "document",
+					Relations: map[string]*openfgapb.Userset{
+						"parent": typesystem.This(),
+						"viewer": typesystem.TupleToUserset("parent", "viewer"),
+					},
+					Metadata: &openfgapb.Metadata{
+						Relations: map[string]*openfgapb.RelationMetadata{
+							"parent": {
+								DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
+									typesystem.RelationReference("folder", ""),
+								},
+							},
+						},
+					},
+				},
+				{
+					Type: "folder",
+					Relations: map[string]*openfgapb.Userset{
+						"viewer": typesystem.This(),
+					},
+					Metadata: &openfgapb.Metadata{
+						Relations: map[string]*openfgapb.RelationMetadata{
+							"viewer": {
+								DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
+									typesystem.RelationReference("user", ""),
+								},
+							},
+						},
+					},
+				},
+			},
+			tuples: []*openfgapb.TupleKey{
+				tuple.NewTupleKey("document:doc1", "parent", "*"), // wildcard not allowed on tupleset relations
+				tuple.NewTupleKey("folder:folder1", "viewer", "user:anne"),
+			},
+			err: serverErrors.InvalidTuple(
+				"unexpected wildcard evaluated on relation 'document#parent'",
+				tuple.NewTupleKey("document:doc1", "parent", commands.Wildcard),
+			),
+		},
+		{
+			name:             "Error if * encountered in TTU evaluation including ContextualTuples",
+			resolveNodeLimit: defaultResolveNodeLimit,
+			request: &openfgapb.CheckRequest{
+				TupleKey: tuple.NewTupleKey("document:doc1", "viewer", "user:anne"),
+				ContextualTuples: &openfgapb.ContextualTupleKeys{
+					TupleKeys: []*openfgapb.TupleKey{
+						tuple.NewTupleKey("document:doc1", "parent", "*"), // wildcard not allowed on tupleset relations
+						tuple.NewTupleKey("folder:folder1", "viewer", "user:anne"),
+					},
+				},
+			},
+			typeDefinitions: []*openfgapb.TypeDefinition{
+				{
+					Type: "document",
+					Relations: map[string]*openfgapb.Userset{
+						"parent": typesystem.This(),
+						"viewer": typesystem.TupleToUserset("parent", "viewer"),
+					},
+					Metadata: &openfgapb.Metadata{
+						Relations: map[string]*openfgapb.RelationMetadata{
+							"parent": {
+								DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
+									typesystem.RelationReference("folder", ""),
+								},
+							},
+						},
+					},
+				},
+				{
+					Type: "folder",
+					Relations: map[string]*openfgapb.Userset{
+						"viewer": typesystem.This(),
+					},
+					Metadata: &openfgapb.Metadata{
+						Relations: map[string]*openfgapb.RelationMetadata{
+							"viewer": {
+								DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
+									typesystem.RelationReference("user", ""),
+								},
+							},
+						},
+					},
+				},
+			},
+			err: serverErrors.InvalidTuple(
+				"unexpected wildcard evaluated on relation 'document#parent'",
+				tuple.NewTupleKey("document:doc1", "parent", commands.Wildcard),
+			),
+		},
 	}
 
 	ctx := context.Background()
 	tracer := telemetry.NewNoopTracer()
 	meter := telemetry.NewNoopMeter()
@@ -1194,17 +1292,11 @@ func TestCheckQuery(t *testing.T, datastore storage.OpenFGADatastore) {
 			cmd := commands.NewCheckQuery(datastore, tracer, meter, logger, test.resolveNodeLimit)
 			test.request.StoreId = store
 			test.request.AuthorizationModelId = model.Id
 			resp, gotErr := cmd.Execute(ctx, test.request)
 
-			if test.err == nil {
-				require.NoError(t, gotErr)
-			}
-
-			if test.err != nil {
-				require.EqualError(t, test.err, gotErr.Error())
-			}
+			require.ErrorIs(t, gotErr, test.err)
 
 			if test.response != nil {
 				require.NoError(t, gotErr)
 
 				require.Equal(t, test.response.Allowed, resp.Allowed)
diff --git a/server/test/expand.go b/server/test/expand.go
index b5486e4..93ca153 100644
--- a/server/test/expand.go
+++ b/server/test/expand.go
@@ -2,16 +2,16 @@ package test
 
 import (
 	"context"
 	"testing"
 
-	"github.com/go-errors/errors"
 	"github.com/google/go-cmp/cmp"
 	"github.com/openfga/openfga/pkg/id"
 	"github.com/openfga/openfga/pkg/logger"
 	"github.com/openfga/openfga/pkg/telemetry"
 	"github.com/openfga/openfga/pkg/testutils"
+	"github.com/openfga/openfga/pkg/tuple"
 	"github.com/openfga/openfga/pkg/typesystem"
 	"github.com/openfga/openfga/server/commands"
 	serverErrors "github.com/openfga/openfga/server/errors"
 	"github.com/openfga/openfga/storage"
 	"github.com/stretchr/testify/require"
@@ -809,10 +809,35 @@ func TestExpandQueryErrors(t *testing.T, datastore storage.OpenFGADatastore) {
 			expected: serverErrors.RelationNotFound("baz", "repo", &openfgapb.TupleKey{
 				Object:   "repo:bar",
 				Relation: "baz",
 			}),
 		},
+		{
+			name: "TupleToUserset involving wildcard returns error",
+			typeDefinitions: []*openfgapb.TypeDefinition{
+				{
+					Type: "document",
+					Relations: map[string]*openfgapb.Userset{
+						"parent": typesystem.This(),
+						"viewer": typesystem.Union(
+							typesystem.This(), typesystem.TupleToUserset("parent", "viewer"),
+						),
+					},
+				},
+			},
+			tuples: []*openfgapb.TupleKey{
+				tuple.NewTupleKey("document:1", "parent", "*"),
+				tuple.NewTupleKey("document:X", "viewer", "jon"),
+			},
+			request: &openfgapb.ExpandRequest{
+				TupleKey: tuple.NewTupleKey("document:1", "viewer", ""),
+			},
+			expected: serverErrors.InvalidTuple(
+				"unexpected wildcard evaluated on relation 'document#parent'",
+				tuple.NewTupleKey("document:1", "parent", "*"),
+			),
+		},
 	}
 
 	require := require.New(t)
 	ctx := context.Background()
 	tracer := telemetry.NewNoopTracer()
@@ -828,11 +853,9 @@ func TestExpandQueryErrors(t *testing.T, datastore storage.OpenFGADatastore) {
 			query := commands.NewExpandQuery(datastore, tracer, logger)
 			test.request.StoreId = store
 			test.request.AuthorizationModelId = modelID
 
 			_, err = query.Execute(ctx, test.request)
-			if !errors.Is(err, test.expected) {
-				t.Fatalf("'%s': Execute(), err = %v, want %v", test.name, err, test.expected)
-			}
+			require.ErrorIs(err, test.expected)
 		})
 	}
 }

Vulnerable code: const AllUsers = "*"
	findTK := &openfgapb.TupleKey{Object: rc.tk.GetObject(), Relation: relation}
		if rt, found = u.u[AllUsers]; !found {
func (query *ExpandQuery) resolveUserset(ctx context.Context, store, modelID string, userset *openfgapb.Userset, tk *openfgapb.TupleKey, metadata *utils.ResolutionMetadata) (*openfgapb.UsersetTree_Node, error) {
		return query.resolveTupleToUserset(ctx, store, us.TupleToUserset, tk, metadata)
func (query *ExpandQuery) resolveTupleToUserset(ctx context.Context, store string, userset *openfgapb.TupleToUserset, tk *openfgapb.TupleKey, metadata *utils.ResolutionMetadata) (*openfgapb.UsersetTree_Node, error) {
		Object:   tk.GetObject(),
		Relation: userset.GetTupleset().GetRelation(),
			if test.err == nil {
				require.NoError(t, gotErr)
			}

			if test.err != nil {
				require.EqualError(t, test.err, gotErr.Error())
			}
	"github.com/go-errors/errors"
			if !errors.Is(err, test.expected) {
				t.Fatalf("'%s': Execute(), err = %v, want %v", test.name, err, test.expected)
			}
Fixed code: 	"fmt"
const Wildcard = "*"

	findTK := tupleUtils.NewTupleKey(rc.tk.GetObject(), relation, "")

		if userObj == Wildcard {
			objectType, _ := tupleUtils.SplitObject(rc.tk.GetObject())

			query.logger.WarnWithContext(
				ctx,
				fmt.Sprintf("unexpected wildcard evaluated on tupleset relation '%s'", relation),
				zap.String("store_id", rc.store),
				zap.String("authorization_model_id", rc.modelID),
				zap.String("object_type", objectType),
			)

			return serverErrors.InvalidTuple(
				fmt.Sprintf("unexpected wildcard evaluated on relation '%s#%s'", objectType, relation),
				tupleUtils.NewTupleKey(rc.tk.GetObject(), relation, Wildcard),
			)
		}

		if rt, found = u.u[Wildcard]; !found {
	"fmt"
	"go.uber.org/zap"
func (query *ExpandQuery) resolveUserset(
	ctx context.Context,
	store, modelID string,
	userset *openfgapb.Userset,
	tk *openfgapb.TupleKey,
	metadata *utils.ResolutionMetadata,
) (*openfgapb.UsersetTree_Node, error) {
		return query.resolveTupleToUserset(ctx, store, modelID, us.TupleToUserset, tk, metadata)
func (query *ExpandQuery) resolveTupleToUserset(
	ctx context.Context,
	store, modelID string,
	userset *openfgapb.TupleToUserset,
	tk *openfgapb.TupleKey,
	metadata *utils.ResolutionMetadata,
) (*openfgapb.UsersetTree_Node, error) {
	targetObject := tk.GetObject()

	tupleset := userset.GetTupleset().GetRelation()
		Object:   targetObject,
		Relation: tupleset,



		if user == Wildcard {
			objectType, _ := tupleUtils.SplitObject(targetObject)

			query.logger.WarnWithContext(
				ctx,
				fmt.Sprintf("unexpected wildcard evaluated on tupleset relation '%s'", tupleset),
				zap.String("store_id", store),
				zap.String("authorization_model_id", modelID),
				zap.String("object_type", objectType),
			)

			return nil, serverErrors.InvalidTuple(
				fmt.Sprintf("unexpected wildcard evaluated on relation '%s#%s'", objectType, tupleset),
				tupleUtils.NewTupleKey(targetObject, tupleset, Wildcard),
			)
		}






		{
			name:             "Error if * encountered in TupleToUserset evaluation",
			resolveNodeLimit: defaultResolveNodeLimit,
			request: &openfgapb.CheckRequest{
				TupleKey: tuple.NewTupleKey("document:doc1", "viewer", "user:anne"),
			},
			typeDefinitions: []*openfgapb.TypeDefinition{
				{
					Type: "document",
					Relations: map[string]*openfgapb.Userset{
						"parent": typesystem.This(),
						"viewer": typesystem.TupleToUserset("parent", "viewer"),
					},
					Metadata: &openfgapb.Metadata{
						Relations: map[string]*openfgapb.RelationMetadata{
							"parent": {
								DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
									typesystem.RelationReference("folder", ""),
								},
							},
						},
					},
				},
				{
					Type: "folder",
					Relations: map[string]*openfgapb.Userset{
						"viewer": typesystem.This(),
					},
					Metadata: &openfgapb.Metadata{
						Relations: map[string]*openfgapb.RelationMetadata{
							"viewer": {
								DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
									typesystem.RelationReference("user", ""),
								},
							},
						},
					},
				},
			},
			tuples: []*openfgapb.TupleKey{
				tuple.NewTupleKey("document:doc1", "parent", "*"), // wildcard not allowed on tupleset relations
				tuple.NewTupleKey("folder:folder1", "viewer", "user:anne"),
			},
			err: serverErrors.InvalidTuple(
				"unexpected wildcard evaluated on relation 'document#parent'",
				tuple.NewTupleKey("document:doc1", "parent", commands.Wildcard),
			),
		},
		{
			name:             "Error if * encountered in TTU evaluation including ContextualTuples",
			resolveNodeLimit: defaultResolveNodeLimit,
			request: &openfgapb.CheckRequest{
				TupleKey: tuple.NewTupleKey("document:doc1", "viewer", "user:anne"),
				ContextualTuples: &openfgapb.ContextualTupleKeys{
					TupleKeys: []*openfgapb.TupleKey{
						tuple.NewTupleKey("document:doc1", "parent", "*"), // wildcard not allowed on tupleset relations
						tuple.NewTupleKey("folder:folder1", "viewer", "user:anne"),
					},
				},
			},
			typeDefinitions: []*openfgapb.TypeDefinition{
				{
					Type: "document",
					Relations: map[string]*openfgapb.Userset{
						"parent": typesystem.This(),
						"viewer": typesystem.TupleToUserset("parent", "viewer"),
					},
					Metadata: &openfgapb.Metadata{
						Relations: map[string]*openfgapb.RelationMetadata{
							"parent": {
								DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
									typesystem.RelationReference("folder", ""),
								},
							},
						},
					},
				},
				{
					Type: "folder",
					Relations: map[string]*openfgapb.Userset{
						"viewer": typesystem.This(),
					},
					Metadata: &openfgapb.Metadata{
						Relations: map[string]*openfgapb.RelationMetadata{
							"viewer": {
								DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
									typesystem.RelationReference("user", ""),
								},
							},
						},
					},
				},
			},
			err: serverErrors.InvalidTuple(
				"unexpected wildcard evaluated on relation 'document#parent'",
				tuple.NewTupleKey("document:doc1", "parent", commands.Wildcard),
			),
		},
			require.ErrorIs(t, gotErr, test.err)
	"github.com/openfga/openfga/pkg/tuple"
		{
			name: "TupleToUserset involving wildcard returns error",
			typeDefinitions: []*openfgapb.TypeDefinition{
				{
					Type: "document",
					Relations: map[string]*openfgapb.Userset{
						"parent": typesystem.This(),
						"viewer": typesystem.Union(
							typesystem.This(), typesystem.TupleToUserset("parent", "viewer"),
						),
					},
				},
			},
			tuples: []*openfgapb.TupleKey{
				tuple.NewTupleKey("document:1", "parent", "*"),
				tuple.NewTupleKey("document:X", "viewer", "jon"),
			},
			request: &openfgapb.ExpandRequest{
				TupleKey: tuple.NewTupleKey("document:1", "viewer", ""),
			},
			expected: serverErrors.InvalidTuple(
				"unexpected wildcard evaluated on relation 'document#parent'",
				tuple.NewTupleKey("document:1", "parent", "*"),
			),
		},
			require.ErrorIs(err, test.expected)